@nextclaw/ui 0.15.6 → 0.15.7
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 +17 -0
- package/dist/assets/{api-Dyb5rgh0.js → api-V8m62-13.js} +1 -1
- package/dist/assets/{app-presenter-provider-7jV0nPiL.js → app-presenter-provider-Bzj9h_ZS.js} +21 -21
- package/dist/assets/{appearance-settings-page-DFm3BbEb.js → appearance-settings-page-DwBwu1AU.js} +1 -1
- package/dist/assets/{book-open-L6LCuqOr.js → book-open-DtVSr5O9.js} +1 -1
- package/dist/assets/{channels-list-page-DOaPKNJA.js → channels-list-page-CYIqMliJ.js} +1 -1
- package/dist/assets/{chat-page-Cw9uDhD_.js → chat-page-BC0icTtI.js} +1 -1
- package/dist/assets/{config-split-page-CqlqjnCK.js → config-split-page-B88prd8s.js} +1 -1
- package/dist/assets/{confirm-dialog-bAp8NU93.js → confirm-dialog-BB09IC2u.js} +1 -1
- package/dist/assets/{desktop-update-config-2SjMfCMl.js → desktop-update-config-B-q0OhxO.js} +1 -1
- package/dist/assets/{dist-CrtbGLlh.js → dist-Bytser-a.js} +1 -1
- package/dist/assets/{doc-browser-BmnQa30-.js → doc-browser-B1FXvZm6.js} +1 -1
- package/dist/assets/doc-browser-Bodhf0fj2.js +1 -0
- package/dist/assets/{doc-browser-context-BlpUUFrR.js → doc-browser-context-D5VLNoj3.js} +1 -1
- package/dist/assets/{download-BhZZNXD8.js → download-CdO6NoHj.js} +1 -1
- package/dist/assets/{external-link-B9Uoq5J7.js → external-link-C5NFoh5I.js} +1 -1
- package/dist/assets/{eye-K5knauEM.js → eye-B51oNRRc.js} +1 -1
- package/dist/assets/{folder-BIYZZWJv.js → folder-DxH7Nef-.js} +1 -1
- package/dist/assets/{form-actions-CKprWFeM.js → form-actions-C9pUwtYV.js} +1 -1
- package/dist/assets/index-DtWf7kk9.css +1 -0
- package/dist/assets/index-UnY4HtVa.js +2 -0
- package/dist/assets/loader-circle-BRSfaztA.js +1 -0
- package/dist/assets/{logo-badge-BQ-XFZod.js → logo-badge-D2vORX3a.js} +1 -1
- package/dist/assets/{mcp-marketplace-page-BfA7A3n4.js → mcp-marketplace-page-CGVu7mfL.js} +1 -1
- package/dist/assets/mcp-marketplace-page-DnV0e1JS.js +1 -0
- package/dist/assets/{model-config-page-BbWbW_qM.js → model-config-page-DYpchGVE.js} +1 -1
- package/dist/assets/plus-BgMBV9HJ.js +1 -0
- package/dist/assets/{popover-CX9zFsuc.js → popover-CONujDHf.js} +1 -1
- package/dist/assets/{provider-scoped-model-input-B-9_DtIP.js → provider-scoped-model-input-DA3Ua9v8.js} +1 -1
- package/dist/assets/{providers-config-page-xwIjGur2.js → providers-config-page-DXsoyMrh.js} +1 -1
- package/dist/assets/{react-DsgudwpT.js → react-Io6Pfq8D.js} +1 -1
- package/dist/assets/{refresh-cw-kXdvbhFf.js → refresh-cw-C6j2tjul.js} +1 -1
- package/dist/assets/remote-C1y3ARHu.js +1 -0
- package/dist/assets/rotate-cw-Bv2B6fAV.js +1 -0
- package/dist/assets/{runtime-config-page-DTsi7czJ.js → runtime-config-page-eYrrKBrA.js} +1 -1
- package/dist/assets/{save-C9qpz2_2.js → save-CBBZVvZ-.js} +1 -1
- package/dist/assets/{search-_kEi29qo.js → search-DFF842z2.js} +1 -1
- package/dist/assets/{search-config-page-H7ug8f4D.js → search-config-page-DUyuwD7d.js} +1 -1
- package/dist/assets/{secrets-config-page-C1_6rINu.js → secrets-config-page-DwzKQJ76.js} +1 -1
- package/dist/assets/security-config-D8me3pXl.js +1 -0
- package/dist/assets/{select-MJKhKAWR.js → select-BiOpo2kV.js} +1 -1
- package/dist/assets/{setting-row-DA3tJuew.js → setting-row-C3WQXLtS.js} +1 -1
- package/dist/assets/{settings-2-D_y7zXJz.js → settings-2-DyOvA_6K.js} +1 -1
- package/dist/assets/{skeleton-0uTLUhm9.js → skeleton-CQIAEbA4.js} +1 -1
- package/dist/assets/{tabs-h44XPocd.js → tabs-BTSENlI2.js} +1 -1
- package/dist/assets/{tag-chip-bRZ04q3C.js → tag-chip-BG5JIGwY.js} +1 -1
- package/dist/assets/{trash-2-BRpSE-Wt.js → trash-2-CTIfwpt1.js} +1 -1
- package/dist/assets/{use-config-CwDEAl6f.js → use-config-CY4uF2Ou.js} +1 -1
- package/dist/assets/{use-confirm-dialog-BbnWw-8c.js → use-confirm-dialog-Bj4u78F9.js} +1 -1
- package/dist/assets/{use-viewport-layout-CqgIMDQL.js → use-viewport-layout-ChJUcSQ6.js} +1 -1
- package/dist/assets/x-C6cM8wkT.js +1 -0
- package/dist/index.html +30 -29
- package/index.html +1 -0
- package/package.json +8 -8
- package/src/features/chat/components/conversation/__tests__/chat-conversation-header-section.test.tsx +13 -2
- package/src/features/chat/features/conversation/components/__tests__/session-conversation-input.streaming.test.tsx +18 -0
- package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +47 -2
- package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +63 -16
- package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +54 -4
- package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +4 -4
- package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +1 -3
- package/src/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils.ts +110 -27
- package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +17 -1
- package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +17 -0
- package/src/features/chat/features/message/components/__tests__/chat-inline-file-preview.test.tsx +159 -0
- package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +92 -26
- package/src/features/chat/features/message/components/chat-inline-file-preview.tsx +90 -21
- package/src/features/chat/features/message/components/chat-message-list.container.tsx +24 -3
- package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +1 -0
- package/src/features/chat/features/ncp/hooks/__tests__/use-hydrated-ncp-agent.test.tsx +55 -0
- package/src/features/chat/features/session/components/session-header/chat-session-title-switcher.tsx +1 -1
- package/src/features/chat/features/workspace/components/__tests__/chat-session-workspace-file-preview.test.tsx +5 -6
- package/src/features/chat/features/workspace/components/__tests__/workspace-file-content-preview.test.tsx +67 -0
- package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +9 -7
- package/src/features/chat/features/workspace/components/workspace-file-content-preview.tsx +95 -4
- package/src/features/marketplace/components/__tests__/marketplace-page-detail.test.tsx +1 -0
- package/src/features/marketplace/components/__tests__/marketplace-page.test.tsx +62 -0
- package/src/features/marketplace/components/curated-shelves/__tests__/marketplace-curated-scene-route.test.tsx +1 -0
- package/src/features/marketplace/components/curated-shelves/marketplace-curated-shelves.tsx +2 -15
- package/src/features/marketplace/components/marketplace-page.tsx +10 -2
- package/src/features/marketplace/hooks/__tests__/use-marketplace.test.tsx +26 -1
- package/src/features/marketplace/hooks/use-marketplace-curated-scene-route.ts +11 -0
- package/src/features/marketplace/hooks/use-marketplace-list-model.ts +1 -1
- package/src/features/marketplace/hooks/use-marketplace.ts +28 -1
- package/src/shared/lib/i18n/locales/en-US/chat.json +5 -0
- package/src/shared/lib/i18n/locales/en-US/desktop-update.json +1 -1
- package/src/shared/lib/i18n/locales/en-US/marketplace.json +1 -0
- package/src/shared/lib/i18n/locales/zh-CN/chat.json +5 -0
- package/src/shared/lib/i18n/locales/zh-CN/desktop-update.json +1 -1
- package/src/shared/lib/i18n/locales/zh-CN/marketplace.json +1 -0
- package/dist/assets/doc-browser-A5IRyDXQ2.js +0 -1
- package/dist/assets/index-BVn702m4.css +0 -1
- package/dist/assets/index-CFxJ1Fkg.js +0 -2
- package/dist/assets/loader-circle-Bj2RMrCR.js +0 -1
- package/dist/assets/mcp-marketplace-page-CXHYO95j.js +0 -1
- package/dist/assets/plus-TmFdwO3P.js +0 -1
- package/dist/assets/remote-B9F2uCk9.js +0 -1
- package/dist/assets/rotate-cw-DEc6aihr.js +0 -1
- package/dist/assets/security-config-Cr2ERKLu.js +0 -1
- package/dist/assets/x-DdEDL6ju.js +0 -1
|
@@ -4,7 +4,8 @@ import {
|
|
|
4
4
|
buildInlineTokensFromComposer,
|
|
5
5
|
CHAT_INLINE_TOKENS_METADATA_KEY,
|
|
6
6
|
readInlineTokensFromMetadata,
|
|
7
|
-
resolveInlineTokensForText
|
|
7
|
+
resolveInlineTokensForText,
|
|
8
|
+
resolveWorkspaceReferencePath,
|
|
8
9
|
} from '@/features/chat/features/input/utils/chat-inline-token.utils';
|
|
9
10
|
|
|
10
11
|
describe('chat-inline-token utils', () => {
|
|
@@ -143,4 +144,19 @@ describe('chat-inline-token utils', () => {
|
|
|
143
144
|
},
|
|
144
145
|
]);
|
|
145
146
|
});
|
|
147
|
+
|
|
148
|
+
it('resolves workspace token paths inside POSIX and Windows project roots', () => {
|
|
149
|
+
expect(resolveWorkspaceReferencePath({
|
|
150
|
+
projectRoot: '/tmp/project/',
|
|
151
|
+
relativePath: 'docs/guide.md',
|
|
152
|
+
})).toBe('/tmp/project/docs/guide.md');
|
|
153
|
+
expect(resolveWorkspaceReferencePath({
|
|
154
|
+
projectRoot: 'C:\\workspace\\nextclaw\\',
|
|
155
|
+
relativePath: 'docs/guide.md',
|
|
156
|
+
})).toBe('C:\\workspace\\nextclaw\\docs\\guide.md');
|
|
157
|
+
expect(resolveWorkspaceReferencePath({
|
|
158
|
+
projectRoot: '/tmp/project',
|
|
159
|
+
relativePath: '../secret.txt',
|
|
160
|
+
})).toBeNull();
|
|
161
|
+
});
|
|
146
162
|
});
|
|
@@ -15,6 +15,23 @@ const CHAT_WORKSPACE_DIRECTORY_TOKEN_PATTERN = /@folder:([^\s]+)/g;
|
|
|
15
15
|
|
|
16
16
|
export type ChatInlineTokenSource = ChatInlineTokenMetadata;
|
|
17
17
|
|
|
18
|
+
export function resolveWorkspaceReferencePath(params: {
|
|
19
|
+
projectRoot: string | null | undefined;
|
|
20
|
+
relativePath: string;
|
|
21
|
+
}): string | null {
|
|
22
|
+
const projectRoot = params.projectRoot?.trim().replace(/[\\/]+$/, '') ?? '';
|
|
23
|
+
const relativeSegments = params.relativePath.trim().replace(/\\/g, '/').split('/');
|
|
24
|
+
if (
|
|
25
|
+
!projectRoot ||
|
|
26
|
+
relativeSegments.length === 0 ||
|
|
27
|
+
relativeSegments.some((segment) => !segment || segment === '.' || segment === '..')
|
|
28
|
+
) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
const separator = projectRoot.includes('\\') ? '\\' : '/';
|
|
32
|
+
return `${projectRoot}${separator}${relativeSegments.join(separator)}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
18
35
|
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
19
36
|
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
20
37
|
}
|
package/src/features/chat/features/message/components/__tests__/chat-inline-file-preview.test.tsx
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { act, fireEvent, render, screen } from "@testing-library/react";
|
|
2
|
+
import { beforeEach, expect, it, vi } from "vitest";
|
|
3
|
+
import { ChatInlineFilePreview } from "@/features/chat/features/message/components/chat-inline-file-preview";
|
|
4
|
+
|
|
5
|
+
const captures = vi.hoisted(() => ({
|
|
6
|
+
filePreviewProps: [] as Array<{
|
|
7
|
+
showBreadcrumbs?: boolean;
|
|
8
|
+
onHtmlContentHeightChange?: (height: number) => void;
|
|
9
|
+
}>,
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
vi.mock(
|
|
13
|
+
"@/features/chat/features/workspace/components/chat-session-workspace-file-preview",
|
|
14
|
+
() => ({
|
|
15
|
+
ChatSessionWorkspaceFilePreview: (props: {
|
|
16
|
+
showBreadcrumbs?: boolean;
|
|
17
|
+
onHtmlContentHeightChange?: (height: number) => void;
|
|
18
|
+
}) => {
|
|
19
|
+
captures.filePreviewProps.push(props);
|
|
20
|
+
return <div data-testid="inline-workspace-file-preview" />;
|
|
21
|
+
},
|
|
22
|
+
}),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
vi.mock("@/shared/lib/i18n", () => ({
|
|
26
|
+
t: (key: string) => key,
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
beforeEach(() => {
|
|
30
|
+
captures.filePreviewProps = [];
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("renders adaptive inline HTML without persistent file chrome", () => {
|
|
34
|
+
const onFileOpen = vi.fn();
|
|
35
|
+
const { container } = render(
|
|
36
|
+
<ChatInlineFilePreview
|
|
37
|
+
display={{
|
|
38
|
+
target: {
|
|
39
|
+
type: "file",
|
|
40
|
+
payload: { path: "preview.html", viewer: "rendered" },
|
|
41
|
+
},
|
|
42
|
+
title: "Preview",
|
|
43
|
+
description: "Rendered HTML",
|
|
44
|
+
}}
|
|
45
|
+
parentSessionKey={null}
|
|
46
|
+
sessionProjectRoot={null}
|
|
47
|
+
sessionWorkingDir={null}
|
|
48
|
+
onFileOpen={onFileOpen}
|
|
49
|
+
/>,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
expect(screen.queryByText("Preview")).toBeNull();
|
|
53
|
+
expect(screen.queryByText("preview.html")).toBeNull();
|
|
54
|
+
expect(screen.queryByText("Rendered HTML")).toBeNull();
|
|
55
|
+
const preview = container.querySelector(
|
|
56
|
+
'[data-chat-inline-file-preview="true"]',
|
|
57
|
+
);
|
|
58
|
+
const actions = container.querySelector(
|
|
59
|
+
'[data-chat-inline-file-actions="true"]',
|
|
60
|
+
);
|
|
61
|
+
const viewport = container.querySelector<HTMLElement>(
|
|
62
|
+
'[data-chat-inline-file-viewport="true"]',
|
|
63
|
+
);
|
|
64
|
+
const iframeSurface = screen.getByTestId("inline-workspace-file-preview");
|
|
65
|
+
expect(preview?.classList.contains("border")).toBe(false);
|
|
66
|
+
expect(preview?.classList.contains("pt-9")).toBe(false);
|
|
67
|
+
expect(actions?.classList.contains("left-1/2")).toBe(true);
|
|
68
|
+
expect(actions?.classList.contains("-translate-x-1/2")).toBe(true);
|
|
69
|
+
expect(actions?.classList.contains("bottom-full")).toBe(true);
|
|
70
|
+
expect(actions?.classList.contains("pb-2")).toBe(true);
|
|
71
|
+
expect(actions?.classList.contains("right-2")).toBe(false);
|
|
72
|
+
expect(actions?.classList.contains("opacity-0")).toBe(true);
|
|
73
|
+
expect(actions?.classList.contains("pointer-events-none")).toBe(true);
|
|
74
|
+
expect(
|
|
75
|
+
actions?.classList.contains("group-hover/inline-html:opacity-100"),
|
|
76
|
+
).toBe(true);
|
|
77
|
+
expect(
|
|
78
|
+
actions?.classList.contains("group-hover/inline-html:pointer-events-auto"),
|
|
79
|
+
).toBe(true);
|
|
80
|
+
expect(
|
|
81
|
+
actions?.classList.contains("group-focus-within/inline-html:opacity-100"),
|
|
82
|
+
).toBe(true);
|
|
83
|
+
expect(
|
|
84
|
+
actions?.classList.contains(
|
|
85
|
+
"group-focus-within/inline-html:pointer-events-auto",
|
|
86
|
+
),
|
|
87
|
+
).toBe(true);
|
|
88
|
+
expect(
|
|
89
|
+
container.querySelector('[data-chat-inline-file-actions-surface="true"]'),
|
|
90
|
+
).toBeTruthy();
|
|
91
|
+
expect(viewport?.classList.contains("rounded-lg")).toBe(true);
|
|
92
|
+
expect(viewport?.classList.contains("h-[240px]")).toBe(true);
|
|
93
|
+
expect(viewport?.classList.contains("max-h-[min(80vh,720px)]")).toBe(true);
|
|
94
|
+
expect(captures.filePreviewProps.at(-1)?.showBreadcrumbs).toBe(false);
|
|
95
|
+
expect(captures.filePreviewProps.at(-1)?.onHtmlContentHeightChange).toEqual(
|
|
96
|
+
expect.any(Function),
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
act(() => {
|
|
100
|
+
captures.filePreviewProps.at(-1)?.onHtmlContentHeightChange?.(560);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
expect(viewport?.style.height).toBe("560px");
|
|
104
|
+
expect(screen.getByTestId("inline-workspace-file-preview")).toBe(
|
|
105
|
+
iframeSurface,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
fireEvent.click(screen.getByLabelText("chatPanelCardExpand"));
|
|
109
|
+
fireEvent.click(screen.getByLabelText("chatWorkspaceOpenSource"));
|
|
110
|
+
|
|
111
|
+
expect(onFileOpen).toHaveBeenNthCalledWith(1, {
|
|
112
|
+
path: "preview.html",
|
|
113
|
+
label: "Preview",
|
|
114
|
+
viewMode: "preview",
|
|
115
|
+
previewViewer: "rendered",
|
|
116
|
+
line: undefined,
|
|
117
|
+
column: undefined,
|
|
118
|
+
});
|
|
119
|
+
expect(onFileOpen).toHaveBeenNthCalledWith(2, {
|
|
120
|
+
path: "preview.html",
|
|
121
|
+
label: "Preview",
|
|
122
|
+
viewMode: "preview",
|
|
123
|
+
previewViewer: "source",
|
|
124
|
+
line: undefined,
|
|
125
|
+
column: undefined,
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("keeps file chrome for non-HTML inline previews", () => {
|
|
130
|
+
const { container } = render(
|
|
131
|
+
<ChatInlineFilePreview
|
|
132
|
+
display={{
|
|
133
|
+
target: {
|
|
134
|
+
type: "file",
|
|
135
|
+
payload: { path: "notes.md", viewer: "rendered" },
|
|
136
|
+
},
|
|
137
|
+
title: "Notes",
|
|
138
|
+
}}
|
|
139
|
+
parentSessionKey={null}
|
|
140
|
+
sessionProjectRoot={null}
|
|
141
|
+
sessionWorkingDir={null}
|
|
142
|
+
onFileOpen={vi.fn()}
|
|
143
|
+
/>,
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
expect(screen.getByText("Notes")).toBeTruthy();
|
|
147
|
+
expect(screen.getByText("notes.md")).toBeTruthy();
|
|
148
|
+
expect(
|
|
149
|
+
container
|
|
150
|
+
.querySelector('[data-chat-inline-file-preview="true"]')
|
|
151
|
+
?.classList.contains("border"),
|
|
152
|
+
).toBe(true);
|
|
153
|
+
expect(
|
|
154
|
+
container.querySelector('[data-chat-inline-file-actions="true"]'),
|
|
155
|
+
).toBeNull();
|
|
156
|
+
expect(
|
|
157
|
+
captures.filePreviewProps.at(-1)?.onHtmlContentHeightChange,
|
|
158
|
+
).toBeUndefined();
|
|
159
|
+
});
|
package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { render, screen } from "@testing-library/react";
|
|
1
|
+
import { fireEvent, render, screen } from "@testing-library/react";
|
|
2
2
|
import { isValidElement } from "react";
|
|
3
3
|
import type { NcpMessage } from "@nextclaw/ncp";
|
|
4
4
|
import { beforeEach, expect, it, vi } from "vitest";
|
|
@@ -20,6 +20,7 @@ const captures = vi.hoisted(() => ({
|
|
|
20
20
|
handleToolAction: vi.fn(),
|
|
21
21
|
showContent: vi.fn(),
|
|
22
22
|
filePreviewProps: [] as Array<{ showBreadcrumbs?: boolean }>,
|
|
23
|
+
selectedSession: null as null | { projectRoot: string; workingDir?: string | null },
|
|
23
24
|
}));
|
|
24
25
|
|
|
25
26
|
vi.mock("@nextclaw/agent-chat-ui", async (importOriginal) => {
|
|
@@ -31,6 +32,7 @@ vi.mock("@nextclaw/agent-chat-ui", async (importOriginal) => {
|
|
|
31
32
|
onToolAction?: (action: unknown) => void;
|
|
32
33
|
onFileOpen?: (action: unknown) => void;
|
|
33
34
|
onAttachmentOpen?: (file: unknown) => void;
|
|
35
|
+
onInlineTokenClick?: (token: unknown) => void;
|
|
34
36
|
renderInlineDisplay?: (display: unknown) => unknown;
|
|
35
37
|
renderPanelAppCard?: (panelApp: unknown) => unknown;
|
|
36
38
|
texts?: Record<string, unknown>;
|
|
@@ -53,6 +55,10 @@ vi.mock("@/features/chat/components/providers/chat-presenter.provider", () => ({
|
|
|
53
55
|
}),
|
|
54
56
|
}));
|
|
55
57
|
|
|
58
|
+
vi.mock("@/features/chat/features/ncp/hooks/use-ncp-chat-derived-state", () => ({
|
|
59
|
+
useNcpChatSelectedSession: () => captures.selectedSession,
|
|
60
|
+
}));
|
|
61
|
+
|
|
56
62
|
vi.mock("@/app/components/i18n-provider", () => ({
|
|
57
63
|
useI18n: () => ({ language: captures.language }),
|
|
58
64
|
}));
|
|
@@ -81,6 +87,7 @@ beforeEach(() => {
|
|
|
81
87
|
captures.handleToolAction.mockReset();
|
|
82
88
|
captures.showContent.mockReset();
|
|
83
89
|
captures.filePreviewProps = [];
|
|
90
|
+
captures.selectedSession = null;
|
|
84
91
|
});
|
|
85
92
|
|
|
86
93
|
it("reuses adapted message references when the source message object is unchanged", () => {
|
|
@@ -100,9 +107,7 @@ it("reuses adapted message references when the source message object is unchange
|
|
|
100
107
|
const firstMessages =
|
|
101
108
|
captures.renders[captures.renders.length - 1]?.messages ?? [];
|
|
102
109
|
|
|
103
|
-
rerender(
|
|
104
|
-
<ChatMessageListContainer messages={[message]} isSending={false} />,
|
|
105
|
-
);
|
|
110
|
+
rerender(<ChatMessageListContainer messages={[message]} isSending={false} />);
|
|
106
111
|
|
|
107
112
|
const secondMessages =
|
|
108
113
|
captures.renders[captures.renders.length - 1]?.messages ?? [];
|
|
@@ -223,7 +228,7 @@ it("adds a completed assistant process summary without inventing a duration", ()
|
|
|
223
228
|
toolCallId: "tool-1",
|
|
224
229
|
toolName: "exec_command",
|
|
225
230
|
state: "result",
|
|
226
|
-
args:
|
|
231
|
+
args: '{"cmd":"git status"}',
|
|
227
232
|
result: "clean",
|
|
228
233
|
},
|
|
229
234
|
{
|
|
@@ -270,7 +275,7 @@ it("derives completed assistant process duration from message lifecycle", () =>
|
|
|
270
275
|
toolCallId: "tool-1",
|
|
271
276
|
toolName: "exec_command",
|
|
272
277
|
state: "result",
|
|
273
|
-
args:
|
|
278
|
+
args: '{"cmd":"git status"}',
|
|
274
279
|
result: "clean",
|
|
275
280
|
},
|
|
276
281
|
{
|
|
@@ -322,6 +327,53 @@ it("wires markdown file link actions to the workspace file preview manager", ()
|
|
|
322
327
|
expect(captures.openFilePreview).toHaveBeenCalledWith(action);
|
|
323
328
|
});
|
|
324
329
|
|
|
330
|
+
it("opens workspace file and directory tokens from the message", () => {
|
|
331
|
+
captures.selectedSession = {
|
|
332
|
+
projectRoot: "/tmp/project",
|
|
333
|
+
workingDir: "/tmp/project/packages/ui",
|
|
334
|
+
};
|
|
335
|
+
render(<ChatMessageListContainer messages={[]} isSending={false} />);
|
|
336
|
+
const onInlineTokenClick = captures.renders[captures.renders.length - 1]?.onInlineTokenClick;
|
|
337
|
+
|
|
338
|
+
onInlineTokenClick?.({
|
|
339
|
+
kind: "workspace_file",
|
|
340
|
+
key: "docs/guide.md",
|
|
341
|
+
label: "guide.md",
|
|
342
|
+
rawText: "@file:docs%2Fguide.md",
|
|
343
|
+
});
|
|
344
|
+
onInlineTokenClick?.({
|
|
345
|
+
kind: "workspace_directory",
|
|
346
|
+
key: "packages/server",
|
|
347
|
+
label: "server",
|
|
348
|
+
rawText: "@folder:packages%2Fserver",
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
expect(captures.openFilePreview).toHaveBeenNthCalledWith(1, {
|
|
352
|
+
path: "/tmp/project/docs/guide.md",
|
|
353
|
+
label: "guide.md",
|
|
354
|
+
viewMode: "preview",
|
|
355
|
+
});
|
|
356
|
+
expect(captures.openFilePreview).toHaveBeenNthCalledWith(2, {
|
|
357
|
+
path: "/tmp/project/packages/server",
|
|
358
|
+
label: "server",
|
|
359
|
+
viewMode: "preview",
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
it("opens panel app tokens through the existing content owner", () => {
|
|
364
|
+
render(<ChatMessageListContainer messages={[]} isSending={false} />);
|
|
365
|
+
captures.renders[captures.renders.length - 1]?.onInlineTokenClick?.({
|
|
366
|
+
kind: "panel_app",
|
|
367
|
+
key: "task-board",
|
|
368
|
+
label: "Task Board",
|
|
369
|
+
rawText: "@panel-app:task-board",
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
expect(captures.showContent).toHaveBeenCalledWith({
|
|
373
|
+
target: { type: "panel_app", payload: { appId: "task-board" } },
|
|
374
|
+
});
|
|
375
|
+
});
|
|
376
|
+
|
|
325
377
|
it("opens message attachments in the workspace file preview", () => {
|
|
326
378
|
const message = {
|
|
327
379
|
id: "assistant-attachment",
|
|
@@ -385,12 +437,16 @@ it("renders context inheritance as a divider without repeating inherited message
|
|
|
385
437
|
isSending={false}
|
|
386
438
|
/>,
|
|
387
439
|
);
|
|
388
|
-
const renderedMessages = captures.renders.flatMap(
|
|
440
|
+
const renderedMessages = captures.renders.flatMap(
|
|
441
|
+
(rendered) => rendered.messages,
|
|
442
|
+
);
|
|
389
443
|
const renderedPayload = JSON.stringify(renderedMessages);
|
|
390
444
|
|
|
391
445
|
expect(screen.getByText("chatContextInheritanceInherited")).toBeTruthy();
|
|
392
446
|
expect(container.querySelector('[title*="parent-session"]')).toBeTruthy();
|
|
393
|
-
expect(
|
|
447
|
+
expect(
|
|
448
|
+
container.querySelector('[title*="chatContextInheritanceMessages: 1"]'),
|
|
449
|
+
).toBeTruthy();
|
|
394
450
|
expect(renderedPayload).toContain("child visible request");
|
|
395
451
|
expect(renderedPayload).not.toContain("parent context");
|
|
396
452
|
});
|
|
@@ -416,7 +472,7 @@ it("keeps Hermes tool invocation parts as tool cards instead of flattening them
|
|
|
416
472
|
toolCallId: "hermes-inline-tool-1",
|
|
417
473
|
toolName: "search_files",
|
|
418
474
|
state: "call",
|
|
419
|
-
args:
|
|
475
|
+
args: '{"pattern":"*.py"}',
|
|
420
476
|
},
|
|
421
477
|
{
|
|
422
478
|
type: "text",
|
|
@@ -495,10 +551,12 @@ it("delegates tool actions to the chat thread manager owner", () => {
|
|
|
495
551
|
it("passes the inline panel app renderer to the shared chat UI", () => {
|
|
496
552
|
render(<ChatMessageListContainer messages={[]} isSending={false} />);
|
|
497
553
|
|
|
498
|
-
expect(
|
|
554
|
+
expect(
|
|
555
|
+
captures.renders[captures.renders.length - 1]?.renderPanelAppCard,
|
|
556
|
+
).toEqual(expect.any(Function));
|
|
499
557
|
});
|
|
500
558
|
|
|
501
|
-
it("
|
|
559
|
+
it("connects inline HTML actions to the chat thread manager", () => {
|
|
502
560
|
render(<ChatMessageListContainer messages={[]} isSending={false} />);
|
|
503
561
|
|
|
504
562
|
const renderInlineDisplay =
|
|
@@ -514,24 +572,30 @@ it("renders inline file displays through the workspace file preview", () => {
|
|
|
514
572
|
description: "Rendered HTML",
|
|
515
573
|
});
|
|
516
574
|
|
|
517
|
-
if (!isValidElement
|
|
575
|
+
if (!isValidElement(rendered)) {
|
|
518
576
|
throw new Error("Expected inline file renderer to return a React element");
|
|
519
577
|
}
|
|
520
|
-
expect(rendered.props.display).toEqual({
|
|
521
|
-
target: {
|
|
522
|
-
type: "file",
|
|
523
|
-
payload: { path: "preview.html", viewer: "rendered" },
|
|
524
|
-
},
|
|
525
|
-
title: "Preview",
|
|
526
|
-
description: "Rendered HTML",
|
|
527
|
-
});
|
|
528
|
-
|
|
529
578
|
render(rendered);
|
|
530
579
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
expect(captures.
|
|
580
|
+
fireEvent.click(screen.getByLabelText("chatPanelCardExpand"));
|
|
581
|
+
fireEvent.click(screen.getByLabelText("chatWorkspaceOpenSource"));
|
|
582
|
+
|
|
583
|
+
expect(captures.openFilePreview).toHaveBeenNthCalledWith(1, {
|
|
584
|
+
path: "preview.html",
|
|
585
|
+
label: "Preview",
|
|
586
|
+
viewMode: "preview",
|
|
587
|
+
previewViewer: "rendered",
|
|
588
|
+
line: undefined,
|
|
589
|
+
column: undefined,
|
|
590
|
+
});
|
|
591
|
+
expect(captures.openFilePreview).toHaveBeenNthCalledWith(2, {
|
|
592
|
+
path: "preview.html",
|
|
593
|
+
label: "Preview",
|
|
594
|
+
viewMode: "preview",
|
|
595
|
+
previewViewer: "source",
|
|
596
|
+
line: undefined,
|
|
597
|
+
column: undefined,
|
|
598
|
+
});
|
|
535
599
|
});
|
|
536
600
|
|
|
537
601
|
it("keeps inline panel app displays expandable from the card header", () => {
|
|
@@ -548,7 +612,9 @@ it("keeps inline panel app displays expandable from the card header", () => {
|
|
|
548
612
|
});
|
|
549
613
|
|
|
550
614
|
if (!isValidElement<{ showExpandAction?: boolean }>(rendered)) {
|
|
551
|
-
throw new Error(
|
|
615
|
+
throw new Error(
|
|
616
|
+
"Expected inline panel app renderer to return a React element",
|
|
617
|
+
);
|
|
552
618
|
}
|
|
553
619
|
expect(rendered.props.showExpandAction).toBeUndefined();
|
|
554
620
|
});
|
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
1
2
|
import type {
|
|
2
3
|
ChatFileOpenActionViewModel,
|
|
3
4
|
ChatInlineDisplayViewModel,
|
|
4
5
|
} from "@nextclaw/agent-chat-ui";
|
|
5
|
-
import { FileText } from "lucide-react";
|
|
6
|
+
import { Code2, FileText, PanelRightOpen } from "lucide-react";
|
|
6
7
|
import { ChatSessionWorkspaceFilePreview } from "@/features/chat/features/workspace/components/chat-session-workspace-file-preview";
|
|
7
8
|
import { createWorkspaceFileTab } from "@/features/chat/features/workspace/utils/chat-workspace-file-tab.utils";
|
|
9
|
+
import { IconActionButton } from "@/shared/components/ui/actions/icon-action-button";
|
|
10
|
+
import { t } from "@/shared/lib/i18n";
|
|
11
|
+
import { cn } from "@/shared/lib/utils";
|
|
8
12
|
|
|
9
13
|
type ChatInlineFilePreviewProps = {
|
|
10
14
|
display: ChatInlineDisplayViewModel;
|
|
@@ -14,9 +18,7 @@ type ChatInlineFilePreviewProps = {
|
|
|
14
18
|
onFileOpen: (action: ChatFileOpenActionViewModel) => void;
|
|
15
19
|
};
|
|
16
20
|
|
|
17
|
-
|
|
18
|
-
return path.split(/[\\/]/).filter(Boolean).pop() ?? path;
|
|
19
|
-
}
|
|
21
|
+
const INLINE_HTML_MIN_HEIGHT = 240;
|
|
20
22
|
|
|
21
23
|
export function ChatInlineFilePreview({
|
|
22
24
|
display,
|
|
@@ -25,6 +27,9 @@ export function ChatInlineFilePreview({
|
|
|
25
27
|
sessionWorkingDir,
|
|
26
28
|
onFileOpen,
|
|
27
29
|
}: ChatInlineFilePreviewProps) {
|
|
30
|
+
const [htmlContentHeight, setHtmlContentHeight] = useState<number | null>(
|
|
31
|
+
null,
|
|
32
|
+
);
|
|
28
33
|
if (display.target.type !== "file") {
|
|
29
34
|
return null;
|
|
30
35
|
}
|
|
@@ -43,35 +48,99 @@ export function ChatInlineFilePreview({
|
|
|
43
48
|
if (!file) {
|
|
44
49
|
return null;
|
|
45
50
|
}
|
|
46
|
-
const title =
|
|
51
|
+
const title =
|
|
52
|
+
file.label ?? file.path.split(/[\\/]/).filter(Boolean).pop() ?? file.path;
|
|
53
|
+
const isRenderedHtml =
|
|
54
|
+
file.previewViewer === "rendered" && /\.html?$/i.test(file.path);
|
|
55
|
+
const openInWorkspace = (previewViewer: "rendered" | "source") => {
|
|
56
|
+
onFileOpen({
|
|
57
|
+
path: file.path,
|
|
58
|
+
label: file.label ?? undefined,
|
|
59
|
+
viewMode: "preview",
|
|
60
|
+
previewViewer,
|
|
61
|
+
line: file.line ?? undefined,
|
|
62
|
+
column: file.column ?? undefined,
|
|
63
|
+
});
|
|
64
|
+
};
|
|
47
65
|
|
|
48
66
|
return (
|
|
49
67
|
<section
|
|
50
|
-
className=
|
|
68
|
+
className={cn(
|
|
69
|
+
"my-2 w-full max-w-[48rem]",
|
|
70
|
+
isRenderedHtml
|
|
71
|
+
? "group/inline-html relative overflow-visible"
|
|
72
|
+
: "overflow-hidden rounded-lg border border-border bg-card shadow-sm",
|
|
73
|
+
)}
|
|
51
74
|
data-chat-inline-file-preview="true"
|
|
52
75
|
data-chat-message-wide-content="true"
|
|
53
76
|
>
|
|
54
|
-
|
|
55
|
-
<div
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
</div>
|
|
62
|
-
<p
|
|
63
|
-
className="mt-1 truncate font-mono text-[11px] font-normal text-muted-foreground"
|
|
64
|
-
title={file.path}
|
|
77
|
+
{isRenderedHtml ? (
|
|
78
|
+
<div
|
|
79
|
+
className={cn(
|
|
80
|
+
"pointer-events-none absolute bottom-full left-1/2 z-10 -translate-x-1/2 pb-2 opacity-0 transition-opacity duration-150",
|
|
81
|
+
"group-hover/inline-html:pointer-events-auto group-hover/inline-html:opacity-100 group-focus-within/inline-html:pointer-events-auto group-focus-within/inline-html:opacity-100",
|
|
82
|
+
)}
|
|
83
|
+
data-chat-inline-file-actions="true"
|
|
65
84
|
>
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
85
|
+
<div
|
|
86
|
+
className="flex items-center gap-0.5 rounded-lg bg-background/95 p-0.5 shadow-md ring-1 ring-border/60 backdrop-blur-sm"
|
|
87
|
+
data-chat-inline-file-actions-surface="true"
|
|
88
|
+
>
|
|
89
|
+
<IconActionButton
|
|
90
|
+
size="sm"
|
|
91
|
+
icon={<PanelRightOpen className="h-3.5 w-3.5" />}
|
|
92
|
+
label={t("chatPanelCardExpand")}
|
|
93
|
+
tooltipSide="top"
|
|
94
|
+
onClick={() => openInWorkspace("rendered")}
|
|
95
|
+
/>
|
|
96
|
+
<IconActionButton
|
|
97
|
+
size="sm"
|
|
98
|
+
icon={<Code2 className="h-3.5 w-3.5" />}
|
|
99
|
+
label={t("chatWorkspaceOpenSource")}
|
|
100
|
+
tooltipSide="top"
|
|
101
|
+
onClick={() => openInWorkspace("source")}
|
|
102
|
+
/>
|
|
103
|
+
</div>
|
|
104
|
+
</div>
|
|
105
|
+
) : (
|
|
106
|
+
<header className="border-b border-border bg-muted/45 px-3 py-2">
|
|
107
|
+
<div className="flex min-w-0 items-center gap-2 text-xs font-medium text-foreground">
|
|
108
|
+
<FileText
|
|
109
|
+
className="h-3.5 w-3.5 shrink-0 text-muted-foreground"
|
|
110
|
+
aria-hidden
|
|
111
|
+
/>
|
|
112
|
+
<span className="truncate">{title}</span>
|
|
113
|
+
</div>
|
|
114
|
+
<p
|
|
115
|
+
className="mt-1 truncate font-mono text-[11px] font-normal text-muted-foreground"
|
|
116
|
+
title={file.path}
|
|
117
|
+
>
|
|
118
|
+
{file.path}
|
|
119
|
+
</p>
|
|
120
|
+
</header>
|
|
121
|
+
)}
|
|
122
|
+
<div
|
|
123
|
+
className={cn(
|
|
124
|
+
"min-h-[240px] overflow-hidden",
|
|
125
|
+
isRenderedHtml
|
|
126
|
+
? "h-[240px] max-h-[min(80vh,720px)] rounded-lg"
|
|
127
|
+
: "h-[420px] max-h-[min(60vh,420px)]",
|
|
128
|
+
)}
|
|
129
|
+
data-chat-inline-file-viewport="true"
|
|
130
|
+
style={
|
|
131
|
+
isRenderedHtml && htmlContentHeight
|
|
132
|
+
? { height: Math.max(INLINE_HTML_MIN_HEIGHT, htmlContentHeight) }
|
|
133
|
+
: undefined
|
|
134
|
+
}
|
|
135
|
+
>
|
|
70
136
|
<ChatSessionWorkspaceFilePreview
|
|
71
137
|
file={file}
|
|
72
138
|
sessionProjectRoot={sessionProjectRoot}
|
|
73
139
|
sessionWorkingDir={sessionWorkingDir}
|
|
74
140
|
showBreadcrumbs={false}
|
|
141
|
+
onHtmlContentHeightChange={
|
|
142
|
+
isRenderedHtml ? setHtmlContentHeight : undefined
|
|
143
|
+
}
|
|
75
144
|
onFileOpen={onFileOpen}
|
|
76
145
|
/>
|
|
77
146
|
</div>
|
|
@@ -14,7 +14,10 @@ import {
|
|
|
14
14
|
buildChatMessageAdapterTexts,
|
|
15
15
|
buildChatMessageTexts,
|
|
16
16
|
} from "@/features/chat/features/message/utils/chat-message-texts.utils";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
readInlineTokensFromMetadata,
|
|
19
|
+
resolveWorkspaceReferencePath,
|
|
20
|
+
} from "@/features/chat/features/input/utils/chat-inline-token.utils";
|
|
18
21
|
import { adaptNcpMessageToUiMessage } from "@/features/chat/features/session/utils/ncp-session-adapter.utils";
|
|
19
22
|
import {
|
|
20
23
|
readContextCompactionTimeline,
|
|
@@ -362,9 +365,27 @@ export function ChatMessageListContainer({
|
|
|
362
365
|
const sessionSkillsQuery = useChatQueryStore((state) => state.snapshot.sessionSkillsQuery);
|
|
363
366
|
const handleInlineTokenClick = useCallback(
|
|
364
367
|
(token: ChatInlineTokenViewModel) => {
|
|
365
|
-
if (token.kind
|
|
368
|
+
if (token.kind === "panel_app") {
|
|
369
|
+
void presenter.chatUiManager.showContent({
|
|
370
|
+
target: { type: "panel_app", payload: { appId: token.key } },
|
|
371
|
+
});
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
if (token.kind === "workspace_file" || token.kind === "workspace_directory") {
|
|
375
|
+
const path = resolveWorkspaceReferencePath({
|
|
376
|
+
projectRoot: selectedSession?.projectRoot,
|
|
377
|
+
relativePath: token.key,
|
|
378
|
+
});
|
|
379
|
+
if (path) {
|
|
380
|
+
presenter.chatThreadManager.openFilePreview({
|
|
381
|
+
path,
|
|
382
|
+
label: token.label,
|
|
383
|
+
viewMode: "preview",
|
|
384
|
+
});
|
|
385
|
+
}
|
|
366
386
|
return;
|
|
367
387
|
}
|
|
388
|
+
if (token.kind !== "skill") return;
|
|
368
389
|
const skillKey = token.key.trim();
|
|
369
390
|
if (!skillKey) {
|
|
370
391
|
return;
|
|
@@ -382,7 +403,7 @@ export function ChatMessageListContainer({
|
|
|
382
403
|
previewViewer: "rendered",
|
|
383
404
|
});
|
|
384
405
|
},
|
|
385
|
-
[presenter.chatThreadManager, sessionSkillsQuery],
|
|
406
|
+
[presenter.chatThreadManager, presenter.chatUiManager, selectedSession?.projectRoot, sessionSkillsQuery],
|
|
386
407
|
);
|
|
387
408
|
const handleAttachmentOpen = useCallback(
|
|
388
409
|
(file: { label: string; mimeType: string; dataUrl?: string; sizeBytes?: number; isImage: boolean }) => {
|
|
@@ -107,6 +107,7 @@ export function buildChatMessageTexts(
|
|
|
107
107
|
copyMessageLabel: t("chatMessageCopy"),
|
|
108
108
|
copiedMessageLabel: t("chatMessageCopied"),
|
|
109
109
|
mermaidDiagramLabel: t("chatMermaidDiagram"),
|
|
110
|
+
mermaidLoadingLabel: t("chatMermaidLoading"),
|
|
110
111
|
mermaidRenderErrorLabel: t("chatMermaidRenderError"),
|
|
111
112
|
typingLabel: t("chatTyping"),
|
|
112
113
|
reasoningCharacterCountTemplates: {
|