@nextclaw/ui 0.15.18 → 0.15.20

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.
Files changed (114) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/dist/assets/api-tWUa6l9J.js +13 -0
  3. package/dist/assets/app-presenter-provider-s-FrOinZ.js +106 -0
  4. package/dist/assets/{appearance-settings-page-BkQaPtQ-.js → appearance-settings-page-dMRBD7ni.js} +1 -1
  5. package/dist/assets/{book-open-WsEnuW9P.js → book-open-BQ29ZmDE.js} +1 -1
  6. package/dist/assets/{card-xVrS1b1T.js → card-CtKueklV.js} +1 -1
  7. package/dist/assets/{channels-list-page-CAWuTHSM.js → channels-list-page-C8y3rqYt.js} +2 -2
  8. package/dist/assets/chat-page-MXBltS3G.js +1 -0
  9. package/dist/assets/{config-split-page-xj0UtMow.js → config-split-page-uC6d_r9c.js} +1 -1
  10. package/dist/assets/{confirm-dialog-e2YCUzt1.js → confirm-dialog-C1pbqHZX.js} +1 -1
  11. package/dist/assets/desktop-update-config-DssP8LCk.js +1 -0
  12. package/dist/assets/{dist-Ca-jiFrK.js → dist-rYHnBo8t.js} +1 -1
  13. package/dist/assets/doc-browser-AXF1zRsM.js +1 -0
  14. package/dist/assets/doc-browser-D8ng4EUc.js +1 -0
  15. package/dist/assets/doc-browser-context-CPHNzQ8m.js +1 -0
  16. package/dist/assets/{eye-B30J471A.js → eye-CiJz3ODY.js} +1 -1
  17. package/dist/assets/{form-actions-CnSzu3WD.js → form-actions-BnZVzLZf.js} +1 -1
  18. package/dist/assets/index-CocHWEsI.js +2 -0
  19. package/dist/assets/index-DN6KM7WD.css +1 -0
  20. package/dist/assets/loader-circle-AOpK5kkW.js +1 -0
  21. package/dist/assets/{logo-badge-CISsltAo.js → logo-badge-BXI-Kdhd.js} +1 -1
  22. package/dist/assets/mcp-marketplace-page-5ogeBHnl.js +1 -0
  23. package/dist/assets/mcp-marketplace-page-CxNe-Q9Q.js +9 -0
  24. package/dist/assets/model-config-page-CR_mgEVp.js +1 -0
  25. package/dist/assets/{navigation-link-qPPUiLt0.js → navigation-link-C1cvB7y4.js} +1 -1
  26. package/dist/assets/plus-DI37QuHc.js +1 -0
  27. package/dist/assets/{popover-DHx55dbc.js → popover-ZoDwVwwd.js} +1 -1
  28. package/dist/assets/{provider-scoped-model-input-1TJxAhx6.js → provider-scoped-model-input-DIL6Ex_d.js} +1 -1
  29. package/dist/assets/{providers-config-page-BwHtHIPe.js → providers-config-page-D2NXVctW.js} +1 -1
  30. package/dist/assets/react-39CDNZlM.js +8 -0
  31. package/dist/assets/{refresh-cw-C7bynrWX.js → refresh-cw-DKewcdAl.js} +1 -1
  32. package/dist/assets/remote-DYOOO1YG.js +1 -0
  33. package/dist/assets/rotate-cw-PmqA8LQT.js +1 -0
  34. package/dist/assets/{runtime-config-page-BLrTVO3n.js → runtime-config-page-0lb5NyKc.js} +1 -1
  35. package/dist/assets/{save-CsRsFtHS.js → save-BrckuAXY.js} +1 -1
  36. package/dist/assets/{search-D0HtJIL-.js → search-BhXQB2TO.js} +1 -1
  37. package/dist/assets/{search-config-page-n1GElgij.js → search-config-page-DFOQ6SDJ.js} +1 -1
  38. package/dist/assets/{secrets-config-page-DbUc3Ggi.js → secrets-config-page-BqqeClqE.js} +2 -2
  39. package/dist/assets/security-config-13AtPIlm.js +1 -0
  40. package/dist/assets/{select-C1zfp9zM.js → select-CVzGGT38.js} +1 -1
  41. package/dist/assets/{settings-2-Ba3cLSN2.js → settings-2-BP6L2enG.js} +1 -1
  42. package/dist/assets/skeleton-OwCNQJ7M.js +1 -0
  43. package/dist/assets/{tabs-DmKgMKzb.js → tabs-B-QWjKCL.js} +1 -1
  44. package/dist/assets/{tag-chip-2vwlVUwM.js → tag-chip-C88mxjwx.js} +1 -1
  45. package/dist/assets/{trash-2-CUpWjNHu.js → trash-2-ck3t33I3.js} +1 -1
  46. package/dist/assets/use-config-BAr5bH2c.js +1 -0
  47. package/dist/assets/{use-confirm-dialog-CeUEE4p5.js → use-confirm-dialog-C4e0nG_4.js} +1 -1
  48. package/dist/assets/{use-viewport-layout-DaKJ-kap.js → use-viewport-layout-k1isXfNm.js} +1 -1
  49. package/dist/assets/x-C1ijNWcE.js +1 -0
  50. package/dist/index.html +28 -28
  51. package/package.json +9 -9
  52. package/src/features/chat/features/conversation/hooks/__tests__/use-session-run-queue.test.tsx +28 -0
  53. package/src/features/chat/features/conversation/hooks/use-session-run-queue.ts +4 -5
  54. package/src/features/chat/features/input/hooks/__tests__/use-chat-input-surface-state.test.tsx +13 -0
  55. package/src/features/chat/features/input/hooks/use-chat-input-surface-state.ts +31 -8
  56. package/src/features/chat/features/input/input-surface-plugins/__tests__/context-reference-plugin.test.ts +79 -9
  57. package/src/features/chat/features/input/input-surface-plugins/__tests__/slash-command-plugin.test.ts +16 -4
  58. package/src/features/chat/features/input/input-surface-plugins/chat-input-product-plugin-adapters.types.ts +10 -2
  59. package/src/features/chat/features/input/input-surface-plugins/context-reference-items.utils.ts +240 -0
  60. package/src/features/chat/features/input/input-surface-plugins/context-reference-plugin.utils.ts +62 -173
  61. package/src/features/chat/features/input/utils/__tests__/chat-inline-token.utils.test.ts +29 -0
  62. package/src/features/chat/features/input/utils/chat-composer-token-protocol.utils.ts +5 -0
  63. package/src/features/chat/features/input/utils/chat-inline-token.utils.ts +26 -5
  64. package/src/features/chat/features/message/components/__tests__/chat-inline-panel-app-card.test.tsx +35 -0
  65. package/src/features/chat/features/message/components/__tests__/chat-message-list.container.test.tsx +12 -1
  66. package/src/features/chat/features/message/components/chat-inline-file-preview.tsx +3 -1
  67. package/src/features/chat/features/message/components/chat-inline-panel-app-card.tsx +13 -5
  68. package/src/features/chat/features/message/components/chat-message-list.container.tsx +10 -5
  69. package/src/features/chat/features/message/utils/__tests__/chat-message-show-content-tool-card.utils.test.ts +9 -0
  70. package/src/features/chat/features/message/utils/chat-inline-panel-app-card.utils.ts +4 -1
  71. package/src/features/chat/features/message/utils/chat-message-show-content-tool-card.utils.ts +29 -0
  72. package/src/features/chat/features/message/utils/chat-message-texts.utils.ts +1 -0
  73. package/src/features/chat/features/ncp/hooks/__tests__/use-ncp-agent-runtime-queue.test.tsx +32 -14
  74. package/src/features/chat/features/workspace/components/__tests__/workspace-file-content-preview.test.tsx +41 -0
  75. package/src/features/chat/features/workspace/components/chat-session-workspace-file-preview.tsx +4 -0
  76. package/src/features/chat/features/workspace/components/workspace-file-content-preview.tsx +15 -2
  77. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab-persistence.utils.ts +11 -0
  78. package/src/features/chat/features/workspace/utils/chat-workspace-file-tab.utils.ts +1 -0
  79. package/src/features/chat/managers/__tests__/chat-thread.manager.test.ts +11 -8
  80. package/src/features/chat/managers/__tests__/chat-ui.manager.test.ts +6 -0
  81. package/src/features/chat/managers/chat-thread.manager.ts +2 -0
  82. package/src/features/chat/managers/chat-ui.manager.ts +10 -4
  83. package/src/features/chat/pages/__tests__/ncp-chat-page.test.ts +26 -0
  84. package/src/features/chat/stores/__tests__/chat-thread.store.test.ts +4 -0
  85. package/src/features/chat/stores/chat-thread.store.ts +6 -1
  86. package/src/shared/components/doc-browser/__tests__/doc-browser-context.test.tsx +32 -0
  87. package/src/shared/components/doc-browser/doc-browser-panel-parts.tsx +2 -0
  88. package/src/shared/components/doc-browser/managers/doc-browser.manager.ts +20 -38
  89. package/src/shared/components/doc-browser/types/doc-browser.types.ts +5 -0
  90. package/src/shared/components/doc-browser/utils/doc-browser-state.utils.ts +63 -0
  91. package/src/shared/hooks/use-projects.ts +2 -1
  92. package/src/shared/lib/i18n/locales/en-US/chat.json +14 -4
  93. package/src/shared/lib/i18n/locales/zh-CN/chat.json +14 -4
  94. package/dist/assets/api-D5UCLVE_.js +0 -13
  95. package/dist/assets/app-presenter-provider-De7Zwitu.js +0 -106
  96. package/dist/assets/chat-page-AvOuaxuv.js +0 -1
  97. package/dist/assets/desktop-update-config-D5NIaimo.js +0 -1
  98. package/dist/assets/doc-browser-B2tNEKrJ.js +0 -1
  99. package/dist/assets/doc-browser-CZX6I6CU.js +0 -1
  100. package/dist/assets/doc-browser-context-CGz0LULR.js +0 -1
  101. package/dist/assets/index-B5AoUrMf.js +0 -2
  102. package/dist/assets/index-QveCjxCj.css +0 -1
  103. package/dist/assets/loader-circle-DjwSRQHW.js +0 -1
  104. package/dist/assets/mcp-marketplace-page-BZa7p7Qf.js +0 -1
  105. package/dist/assets/mcp-marketplace-page-CPcPzMUQ.js +0 -9
  106. package/dist/assets/model-config-page-D6ex3_NM.js +0 -1
  107. package/dist/assets/plus-C45awBb1.js +0 -1
  108. package/dist/assets/react-DerwD2EA.js +0 -8
  109. package/dist/assets/remote-B6TXClIg.js +0 -1
  110. package/dist/assets/rotate-cw-Bhx0XkQP.js +0 -1
  111. package/dist/assets/security-config-n2WHhHXV.js +0 -1
  112. package/dist/assets/skeleton-DL5npsVL.js +0 -1
  113. package/dist/assets/use-config-BvxDWV-M.js +0 -1
  114. package/dist/assets/x-DGdYT7rH.js +0 -1
@@ -150,6 +150,9 @@ describe("buildShowContentToolCard", () => {
150
150
  payload: {
151
151
  appId: "reader",
152
152
  path: "/tmp/reader.panel",
153
+ params: {
154
+ file: { path: "/tmp/photo.png" },
155
+ },
153
156
  },
154
157
  },
155
158
  title: "Reader",
@@ -166,6 +169,9 @@ describe("buildShowContentToolCard", () => {
166
169
  panelApp: {
167
170
  appId: "reader",
168
171
  path: "/tmp/reader.panel",
172
+ params: {
173
+ file: { path: "/tmp/photo.png" },
174
+ },
169
175
  title: "Reader",
170
176
  action: {
171
177
  kind: "show-content",
@@ -176,6 +182,9 @@ describe("buildShowContentToolCard", () => {
176
182
  payload: {
177
183
  appId: "reader",
178
184
  path: "/tmp/reader.panel",
185
+ params: {
186
+ file: { path: "/tmp/photo.png" },
187
+ },
179
188
  },
180
189
  },
181
190
  title: "Reader",
@@ -5,6 +5,7 @@ import {
5
5
  } from "@/features/right-panel-resources";
6
6
  import { PANEL_APP_INLINE_HOST_CONTRACT } from "@nextclaw/shared";
7
7
  import type { DocBrowserTab } from "@/shared/components/doc-browser/doc-browser-context";
8
+ import type { ChatContentParams } from "@nextclaw/agent-chat-ui";
8
9
 
9
10
  export function createFallbackPanelAppContentPath(appId: string, sourcePath?: string): string {
10
11
  return createPanelAppContentPath(appId, sourcePath);
@@ -37,11 +38,13 @@ export function readInlinePanelAppContentHeight(value: unknown): number | null {
37
38
  export function createInlinePanelAppTab(params: {
38
39
  appId: string;
39
40
  path?: string;
41
+ params?: ChatContentParams;
40
42
  title: string;
41
43
  url: string;
42
44
  }): DocBrowserTab {
43
- const { appId, path, title, url } = params;
45
+ const { appId, params: contentParams, path, title, url } = params;
44
46
  return {
47
+ contentParams,
45
48
  currentUrl: url,
46
49
  dedupeKey: `panel-app:${path ?? appId}`,
47
50
  history: [url],
@@ -7,6 +7,10 @@ import type {
7
7
  import type {
8
8
  ToolCardViewSource,
9
9
  } from "./chat-message-tool-card.utils";
10
+ import {
11
+ readUiContentParams,
12
+ type UiContentParams,
13
+ } from "@nextclaw/shared";
10
14
 
11
15
  const SHOW_CONTENT_TOOL_NAMES = ["show_content", "show_file", "show_url", "show_panel_app"];
12
16
 
@@ -56,6 +60,14 @@ function readFileViewer(value: unknown): NonNullable<
56
60
  : undefined;
57
61
  }
58
62
 
63
+ function readContentParams(value: unknown): UiContentParams | undefined | null {
64
+ try {
65
+ return readUiContentParams(value);
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+
59
71
  function readShowContentRequest(value: unknown): ChatUiShowContentRequest | null {
60
72
  if (!isRecord(value) || !isRecord(value.target)) {
61
73
  return null;
@@ -78,6 +90,16 @@ function readShowContentRequest(value: unknown): ChatUiShowContentRequest | null
78
90
  if (!path) {
79
91
  return null;
80
92
  }
93
+ const params = readContentParams(payload.params);
94
+ if (
95
+ params === null ||
96
+ (params && (
97
+ readFileViewer(payload.viewer) === "source" ||
98
+ !/\.html?$/i.test(path)
99
+ ))
100
+ ) {
101
+ return null;
102
+ }
81
103
  return {
82
104
  target: {
83
105
  type: "file",
@@ -86,6 +108,7 @@ function readShowContentRequest(value: unknown): ChatUiShowContentRequest | null
86
108
  line: readOptionalPositiveInteger(payload.line),
87
109
  column: readOptionalPositiveInteger(payload.column),
88
110
  viewer: readFileViewer(payload.viewer),
111
+ params: params ?? undefined,
89
112
  },
90
113
  },
91
114
  title,
@@ -116,12 +139,17 @@ function readShowContentRequest(value: unknown): ChatUiShowContentRequest | null
116
139
  if (!appId) {
117
140
  return null;
118
141
  }
142
+ const params = readContentParams(payload.params);
143
+ if (params === null) {
144
+ return null;
145
+ }
119
146
  return {
120
147
  target: {
121
148
  type: "panel_app",
122
149
  payload: {
123
150
  appId,
124
151
  path: readOptionalString(payload.path),
152
+ params: params ?? undefined,
125
153
  },
126
154
  },
127
155
  title,
@@ -180,6 +208,7 @@ export function buildShowContentToolCard(params: {
180
208
  ? {
181
209
  appId: request.target.payload.appId,
182
210
  path: request.target.payload.path,
211
+ params: request.target.payload.params,
183
212
  title: request.title,
184
213
  action: showContentAction,
185
214
  }
@@ -150,6 +150,7 @@ export function buildChatMessageTexts(
150
150
  copyMessageLabel: t("chatMessageCopy"),
151
151
  copiedMessageLabel: t("chatMessageCopied"),
152
152
  mermaidDiagramLabel: t("chatMermaidDiagram"),
153
+ mermaidExpandLabel: t("chatMermaidExpand"),
153
154
  mermaidLoadingLabel: t("chatMermaidLoading"),
154
155
  mermaidRenderErrorLabel: t("chatMermaidRenderError"),
155
156
  typingLabel: t("chatTyping"),
@@ -1,11 +1,17 @@
1
- import { act, renderHook } from '@testing-library/react';
1
+ import { act, renderHook, waitFor } from '@testing-library/react';
2
2
  import { describe, expect, it, vi } from 'vitest';
3
- import { NcpEventType, type NcpAgentClientEndpoint } from '@nextclaw/ncp';
3
+ import {
4
+ NcpEventType,
5
+ type NcpAgentClientEndpoint,
6
+ type NcpAgentSendEnvelope,
7
+ type NcpEndpointSubscriber,
8
+ type NcpMessage,
9
+ } from '@nextclaw/ncp';
4
10
  import { useNcpAgentRuntime } from '@nextclaw/ncp-react';
5
11
  import { DefaultNcpAgentConversationStateManager } from '@nextclaw/ncp-toolkit';
6
12
 
7
13
  describe('useNcpAgentRuntime backend queue submission', () => {
8
- it('forwards and immediately shows a new message while a run is active', async () => {
14
+ it('keeps a queued message out of the conversation until the backend starts it', async () => {
9
15
  const manager = new DefaultNcpAgentConversationStateManager();
10
16
  manager.hydrate({ sessionId: 'session-1', messages: [] });
11
17
  await manager.dispatch({
@@ -16,18 +22,22 @@ describe('useNcpAgentRuntime backend queue submission', () => {
16
22
  messageId: 'assistant-1',
17
23
  },
18
24
  });
19
- const send = vi.fn(async () => ({
25
+ const send = vi.fn(async (envelope: NcpAgentSendEnvelope) => ({
20
26
  assistantMessageId: null,
21
27
  runId: null,
22
28
  sessionId: 'session-1',
23
- userMessageId: 'user-queued',
29
+ userMessageId: envelope.message.id,
24
30
  }));
31
+ let subscriber: NcpEndpointSubscriber | null = null;
25
32
  const client = {
26
33
  abort: vi.fn(async () => undefined),
27
34
  send,
28
35
  stop: vi.fn(async () => undefined),
29
36
  stream: vi.fn(async () => undefined),
30
- subscribe: vi.fn(() => () => undefined),
37
+ subscribe: vi.fn((listener: NcpEndpointSubscriber) => {
38
+ subscriber = listener;
39
+ return () => undefined;
40
+ }),
31
41
  } as unknown as NcpAgentClientEndpoint;
32
42
  const { result } = renderHook(() => useNcpAgentRuntime({
33
43
  client,
@@ -46,13 +56,21 @@ describe('useNcpAgentRuntime backend queue submission', () => {
46
56
  parts: [{ type: 'text', text: 'queue this' }],
47
57
  }),
48
58
  }));
49
- expect(result.current.visibleMessages).toEqual([
50
- expect.objectContaining({
51
- sessionId: 'session-1',
52
- role: 'user',
53
- status: 'final',
54
- parts: [{ type: 'text', text: 'queue this' }],
55
- }),
56
- ]);
59
+ expect(result.current.visibleMessages).toEqual([]);
60
+
61
+ const queuedMessage = send.mock.calls[0][0].message as NcpMessage;
62
+ await act(async () => {
63
+ subscriber?.({
64
+ type: NcpEventType.MessageSent,
65
+ payload: {
66
+ sessionId: 'session-1',
67
+ message: queuedMessage,
68
+ },
69
+ });
70
+ });
71
+
72
+ await waitFor(() => {
73
+ expect(result.current.visibleMessages).toEqual([queuedMessage]);
74
+ });
57
75
  });
58
76
  });
@@ -87,3 +87,44 @@ it("reports same-origin HTML height changes without replacing the iframe", () =>
87
87
  unmount();
88
88
  expect(disconnect).toHaveBeenCalledTimes(1);
89
89
  });
90
+
91
+ it("passes params through window.name without exposing values in the content URL", () => {
92
+ const { rerender } = render(
93
+ <WorkspaceFileContentPreview
94
+ contentUrl="/api/server-paths/content?path=%2Ftmp%2Fchart.html"
95
+ contentParams={{
96
+ chart: {
97
+ series: [3, 5, 8],
98
+ },
99
+ }}
100
+ kind="html"
101
+ label="chart.html"
102
+ />,
103
+ );
104
+
105
+ const frame = screen.getByTestId(
106
+ "workspace-html-preview",
107
+ ) as HTMLIFrameElement;
108
+ expect(frame.name).toContain('"series":[3,5,8]');
109
+ expect(frame.src).toContain("nextclawContentParams=1");
110
+ expect(frame.src).not.toContain("series");
111
+
112
+ rerender(
113
+ <WorkspaceFileContentPreview
114
+ contentUrl="/api/server-paths/content?path=%2Ftmp%2Fchart.html"
115
+ contentParams={{
116
+ chart: {
117
+ series: [13, 21, 34],
118
+ },
119
+ }}
120
+ kind="html"
121
+ label="chart.html"
122
+ />,
123
+ );
124
+
125
+ const nextFrame = screen.getByTestId(
126
+ "workspace-html-preview",
127
+ ) as HTMLIFrameElement;
128
+ expect(nextFrame).not.toBe(frame);
129
+ expect(nextFrame.name).toContain('"series":[13,21,34]');
130
+ });
@@ -185,6 +185,7 @@ function WorkspacePreviewBody({
185
185
  contentUrl,
186
186
  contentUrlKind,
187
187
  contentLabel,
188
+ contentParams,
188
189
  directoryQuery,
189
190
  fileBasePath,
190
191
  onFileOpen,
@@ -200,6 +201,7 @@ function WorkspacePreviewBody({
200
201
  contentUrl: string | null;
201
202
  contentUrlKind: WorkspaceFileContentKind | null;
202
203
  contentLabel: string;
204
+ contentParams: ChatWorkspaceFileTab["params"];
203
205
  directoryQuery: ReturnType<typeof useServerPathBrowse> | null | undefined;
204
206
  fileBasePath: string | null;
205
207
  onFileOpen: (action: ChatFileOpenActionViewModel) => void;
@@ -216,6 +218,7 @@ function WorkspacePreviewBody({
216
218
  return (
217
219
  <WorkspaceFileContentPreview
218
220
  contentUrl={contentUrl}
221
+ contentParams={contentParams ?? undefined}
219
222
  kind={contentUrlKind}
220
223
  label={contentLabel}
221
224
  onHtmlContentHeightChange={onHtmlContentHeightChange}
@@ -424,6 +427,7 @@ export function ChatSessionWorkspaceFilePreview({
424
427
  contentUrl={contentUrl}
425
428
  contentUrlKind={contentUrlKind}
426
429
  contentLabel={file.label?.trim() || resolvedPath}
430
+ contentParams={file.params}
427
431
  directoryQuery={directoryQuery}
428
432
  fileBasePath={sessionWorkingDir}
429
433
  onFileOpen={onFileOpen}
@@ -1,5 +1,10 @@
1
1
  import { useEffect, useRef } from "react";
2
- import { readInlineContentHeight } from "@nextclaw/shared";
2
+ import {
3
+ appendUiContentParamsBootstrapQuery,
4
+ createUiContentParamsWindowName,
5
+ readInlineContentHeight,
6
+ type UiContentParams,
7
+ } from "@nextclaw/shared";
3
8
  import { WorkspaceDocxPreview } from "./file-content-preview/workspace-docx-preview";
4
9
  import { WorkspacePresentationPreview } from "./file-content-preview/workspace-presentation-preview";
5
10
  import { WorkspaceSpreadsheetPreview } from "./file-content-preview/workspace-spreadsheet-preview";
@@ -119,15 +124,18 @@ function WorkspaceUnsupportedContent({
119
124
 
120
125
  function WorkspaceHtmlPreview({
121
126
  contentUrl,
127
+ contentParams,
122
128
  label,
123
129
  onContentHeightChange,
124
130
  }: {
125
131
  contentUrl: string;
132
+ contentParams?: UiContentParams;
126
133
  label: string;
127
134
  onContentHeightChange?: (height: number) => void;
128
135
  }) {
129
136
  const iframeRef = useRef<HTMLIFrameElement>(null);
130
137
  const observerCleanupRef = useRef<(() => void) | null>(null);
138
+ const contentParamsWindowName = createUiContentParamsWindowName(contentParams);
131
139
 
132
140
  useEffect(
133
141
  () => () => {
@@ -177,10 +185,12 @@ function WorkspaceHtmlPreview({
177
185
  return (
178
186
  <iframe
179
187
  ref={iframeRef}
188
+ key={contentParamsWindowName ?? "no-content-params"}
180
189
  allowFullScreen
181
190
  className="h-full w-full border-0 bg-white"
182
191
  data-testid="workspace-html-preview"
183
- src={contentUrl}
192
+ name={contentParamsWindowName}
193
+ src={appendUiContentParamsBootstrapQuery(contentUrl, contentParams)}
184
194
  title={label}
185
195
  onLoad={observeContentHeight}
186
196
  />
@@ -189,11 +199,13 @@ function WorkspaceHtmlPreview({
189
199
 
190
200
  export function WorkspaceFileContentPreview({
191
201
  contentUrl,
202
+ contentParams,
192
203
  kind,
193
204
  label,
194
205
  onHtmlContentHeightChange,
195
206
  }: {
196
207
  contentUrl: string;
208
+ contentParams?: UiContentParams;
197
209
  kind: WorkspaceFileContentKind;
198
210
  label: string;
199
211
  onHtmlContentHeightChange?: (height: number) => void;
@@ -243,6 +255,7 @@ export function WorkspaceFileContentPreview({
243
255
  return (
244
256
  <WorkspaceHtmlPreview
245
257
  contentUrl={contentUrl}
258
+ contentParams={contentParams}
246
259
  label={label}
247
260
  onContentHeightChange={onHtmlContentHeightChange}
248
261
  />
@@ -1,5 +1,6 @@
1
1
  import type { ChatFilePreviewViewer } from '@nextclaw/agent-chat-ui';
2
2
  import type { ChatWorkspaceFileTab } from '@/features/chat/stores/chat-thread.store';
3
+ import { readUiContentParams } from '@nextclaw/shared';
3
4
 
4
5
  function isRecord(value: unknown): value is Record<string, unknown> {
5
6
  return !!value && typeof value === 'object';
@@ -27,6 +28,14 @@ function normalizeFilePreviewViewer(value: unknown): ChatFilePreviewViewer | nul
27
28
  : null;
28
29
  }
29
30
 
31
+ function normalizeContentParams(value: unknown): ChatWorkspaceFileTab['params'] {
32
+ try {
33
+ return readUiContentParams(value) ?? null;
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
30
39
  export function normalizePersistedWorkspaceFileTab(
31
40
  value: unknown,
32
41
  ): ChatWorkspaceFileTab | null {
@@ -52,6 +61,7 @@ export function normalizePersistedWorkspaceFileTab(
52
61
  previewViewer: normalizeFilePreviewViewer(value.previewViewer),
53
62
  line: normalizeOptionalNumber(value.line),
54
63
  column: normalizeOptionalNumber(value.column),
64
+ params: normalizeContentParams(value.params),
55
65
  rawText: normalizeOptionalText(value.rawText),
56
66
  contentUrl: normalizeOptionalString(value.contentUrl),
57
67
  mimeType: normalizeOptionalString(value.mimeType),
@@ -75,6 +85,7 @@ export function toPersistedWorkspaceFileTab(
75
85
  previewViewer: tab.previewViewer,
76
86
  line: tab.line,
77
87
  column: tab.column,
88
+ params: tab.params,
78
89
  rawText: tab.rawText,
79
90
  contentUrl: tab.contentUrl,
80
91
  mimeType: tab.mimeType,
@@ -29,6 +29,7 @@ export function createWorkspaceFileTab(
29
29
  previewViewer,
30
30
  line: action.line ?? null,
31
31
  column: action.column ?? null,
32
+ params: action.params ?? null,
32
33
  rawText: action.rawText ?? null,
33
34
  contentUrl: action.contentUrl?.trim() || null,
34
35
  mimeType: action.mimeType?.trim() || null,
@@ -301,13 +301,15 @@ describe('ChatThreadManager', () => {
301
301
  );
302
302
 
303
303
  manager.openFilePreview({
304
- path: 'README.md',
305
- label: 'README.md',
304
+ path: 'chart.html',
305
+ label: 'Chart',
306
306
  viewMode: 'preview',
307
+ previewViewer: 'rendered',
308
+ params: { chart: { series: [3, 5, 8] } },
307
309
  });
308
310
  manager.openFilePreview({
309
- path: 'README.md',
310
- label: 'README.md',
311
+ path: 'chart.html',
312
+ label: 'Chart',
311
313
  viewMode: 'diff',
312
314
  beforeText: 'old\n',
313
315
  afterText: 'new\n',
@@ -316,13 +318,14 @@ describe('ChatThreadManager', () => {
316
318
  expect(useChatThreadStore.getState().snapshot.workspaceFileTabs).toEqual(
317
319
  expect.arrayContaining([
318
320
  expect.objectContaining({
319
- key: 'parent-session-1::preview:rendered::README.md',
320
- path: 'README.md',
321
+ key: 'parent-session-1::preview:rendered::chart.html',
322
+ path: 'chart.html',
321
323
  viewMode: 'preview',
324
+ params: { chart: { series: [3, 5, 8] } },
322
325
  }),
323
326
  expect.objectContaining({
324
- key: 'parent-session-1::diff::README.md',
325
- path: 'README.md',
327
+ key: 'parent-session-1::diff::chart.html',
328
+ path: 'chart.html',
326
329
  viewMode: 'diff',
327
330
  }),
328
331
  ]),
@@ -88,6 +88,9 @@ describe('ChatUiManager content display', () => {
88
88
  type: 'panel_app',
89
89
  payload: {
90
90
  appId: 'reader',
91
+ params: {
92
+ file: { path: '/tmp/photo.png' },
93
+ },
91
94
  },
92
95
  },
93
96
  title: 'Reader',
@@ -95,6 +98,9 @@ describe('ChatUiManager content display', () => {
95
98
 
96
99
  expect(docBrowserManager.openTarget).toHaveBeenCalledWith({
97
100
  dedupeKey: 'panel-app:encoded-reader',
101
+ contentParams: {
102
+ file: { path: '/tmp/photo.png' },
103
+ },
98
104
  historyPolicy: 'managed',
99
105
  kind: 'panel-app',
100
106
  resourceUri: 'nextclaw://panel-app/encoded-reader',
@@ -347,6 +347,7 @@ export class ChatThreadManager {
347
347
  previewViewer: nextViewer,
348
348
  line: sourceTab.line ?? undefined,
349
349
  column: sourceTab.column ?? undefined,
350
+ params: sourceTab.params ?? undefined,
350
351
  rawText: sourceTab.rawText ?? undefined,
351
352
  contentUrl: sourceTab.contentUrl ?? undefined,
352
353
  mimeType: sourceTab.mimeType ?? undefined,
@@ -419,6 +420,7 @@ export class ChatThreadManager {
419
420
  previewViewer: request.target.payload.viewer,
420
421
  line: request.target.payload.line,
421
422
  column: request.target.payload.column,
423
+ params: request.target.payload.params,
422
424
  });
423
425
  return;
424
426
  }
@@ -96,21 +96,27 @@ export class ChatUiManager {
96
96
  });
97
97
  return;
98
98
  }
99
- const { appId, path } = target.payload;
99
+ const { appId, params: contentParams, path } = target.payload;
100
100
  if (path) {
101
101
  this.docBrowserManager.open(createPanelAppResourceUri(appId, path), {
102
+ contentParams,
102
103
  title,
103
104
  });
104
105
  return;
105
106
  }
106
107
  const entry = await this.resolvePanelAppEntry(appId);
107
108
  if (entry) {
108
- this.docBrowserManager.openTarget(createPanelAppRightPanelResourceTarget(entry), {
109
- title,
110
- });
109
+ this.docBrowserManager.openTarget(
110
+ {
111
+ ...createPanelAppRightPanelResourceTarget(entry),
112
+ contentParams,
113
+ },
114
+ { title },
115
+ );
111
116
  return;
112
117
  }
113
118
  this.docBrowserManager.open(createPanelAppResourceUri(appId), {
119
+ contentParams,
114
120
  title,
115
121
  });
116
122
  };
@@ -272,6 +272,32 @@ describe('buildChatRunMetadata', () => {
272
272
  });
273
273
  });
274
274
 
275
+ it('adds structured project references to run metadata', () => {
276
+ expect(
277
+ buildChatRunMetadata({
278
+ composerNodes: [
279
+ createChatComposerTokenNode({
280
+ tokenKind: 'project',
281
+ tokenKey: '/tmp/nextclaw',
282
+ label: 'NextClaw',
283
+ }),
284
+ ],
285
+ }),
286
+ ).toMatchObject({
287
+ ui_inline_tokens: {
288
+ schemaVersion: 2,
289
+ items: [
290
+ {
291
+ kind: 'project',
292
+ key: '/tmp/nextclaw',
293
+ label: 'NextClaw',
294
+ rawText: '@project:%2Ftmp%2Fnextclaw',
295
+ },
296
+ ],
297
+ },
298
+ });
299
+ });
300
+
275
301
  it('serializes child session materialization metadata for side chat drafts', () => {
276
302
  expect(
277
303
  buildChatRunMetadata({
@@ -60,6 +60,7 @@ describe('chat thread workspace panel persistence', () => {
60
60
  label: 'README.md',
61
61
  viewMode: 'preview',
62
62
  previewViewer: 'rendered',
63
+ params: { chart: { series: [3, 5, 8] } },
63
64
  rawText: '# Hello',
64
65
  fullLines: [],
65
66
  },
@@ -98,6 +99,9 @@ describe('chat thread workspace panel persistence', () => {
98
99
  expect(persisted.state.snapshot.isSending).toBeUndefined();
99
100
  expect(persisted.state.snapshot.workspaceFileTabs[0].rawText).toBe('# Hello');
100
101
  expect(persisted.state.snapshot.workspaceFileTabs[0].previewViewer).toBe('rendered');
102
+ expect(persisted.state.snapshot.workspaceFileTabs[0].params).toEqual({
103
+ chart: { series: [3, 5, 8] },
104
+ });
101
105
  expect(persisted.state.snapshot.workspaceFileTabs[0].fullLines).toBeUndefined();
102
106
  });
103
107
 
@@ -1,7 +1,11 @@
1
1
  import { create } from 'zustand';
2
2
  import { createJSONStorage, persist } from 'zustand/middleware';
3
3
  import type { NcpMessage } from '@nextclaw/ncp';
4
- import type { ChatFileOperationLineViewModel, ChatFilePreviewViewer } from '@nextclaw/agent-chat-ui';
4
+ import type {
5
+ ChatContentParams,
6
+ ChatFileOperationLineViewModel,
7
+ ChatFilePreviewViewer,
8
+ } from '@nextclaw/agent-chat-ui';
5
9
  import type {
6
10
  SessionContextWindowView,
7
11
  SessionTypeIconView
@@ -38,6 +42,7 @@ export type ChatWorkspaceFileTab = {
38
42
  previewViewer?: ChatFilePreviewViewer | null;
39
43
  line?: number | null;
40
44
  column?: number | null;
45
+ params?: ChatContentParams | null;
41
46
  rawText?: string | null;
42
47
  /** Attachment/binary content URL for workspace-native media preview. */
43
48
  contentUrl?: string | null;
@@ -269,6 +269,9 @@ describe('DocBrowserProvider panel app resources', () => {
269
269
 
270
270
  act(() => {
271
271
  result.current.openTarget({
272
+ contentParams: {
273
+ file: { path: '/tmp/photo.png' },
274
+ },
272
275
  dedupeKey: 'panel-app:demo',
273
276
  dockIcon: { type: 'url', url: '/api/panel-apps/demo/assets/icon.png' },
274
277
  historyPolicy: 'managed',
@@ -281,6 +284,9 @@ describe('DocBrowserProvider panel app resources', () => {
281
284
 
282
285
  expect(result.current.currentTab).toMatchObject({
283
286
  currentUrl: '/api/panel-apps/demo/content',
287
+ contentParams: {
288
+ file: { path: '/tmp/photo.png' },
289
+ },
284
290
  dedupeKey: 'panel-app:demo',
285
291
  dockIcon: { type: 'url', url: '/api/panel-apps/demo/assets/icon.png' },
286
292
  kind: 'panel-app',
@@ -288,6 +294,32 @@ describe('DocBrowserProvider panel app resources', () => {
288
294
  title: 'Demo Panel App',
289
295
  });
290
296
  });
297
+
298
+ it('reloads a reused Panel App tab when its params change', () => {
299
+ const { result } = renderHook(() => useDocBrowser(), { wrapper });
300
+ const openPanelApp = (path: string) => {
301
+ result.current.openTarget({
302
+ contentParams: { file: { path } },
303
+ dedupeKey: 'panel-app:demo',
304
+ historyPolicy: 'managed',
305
+ kind: 'panel-app',
306
+ resourceUri: 'nextclaw://panel-app/demo',
307
+ title: 'Demo Panel App',
308
+ url: '/api/panel-apps/demo/content',
309
+ });
310
+ };
311
+
312
+ act(() => openPanelApp('/tmp/first.png'));
313
+ const firstNavVersion = result.current.currentTab?.navVersion;
314
+ act(() => openPanelApp('/tmp/second.png'));
315
+
316
+ expect(result.current.currentTab).toMatchObject({
317
+ contentParams: {
318
+ file: { path: '/tmp/second.png' },
319
+ },
320
+ navVersion: (firstNavVersion ?? 0) + 1,
321
+ });
322
+ });
291
323
  });
292
324
 
293
325
  describe('DocBrowserProvider managed history', () => {
@@ -10,6 +10,7 @@ import {
10
10
  import { IconActionButton } from '@/shared/components/ui/actions/icon-action-button';
11
11
  import { NavigationLink } from '@/shared/components/actions/navigation-link';
12
12
  import { t } from '@/shared/lib/i18n';
13
+ import { createUiContentParamsWindowName } from '@nextclaw/shared';
13
14
 
14
15
  type DocBrowserAddressToolbarProps = {
15
16
  isVisible: boolean;
@@ -86,6 +87,7 @@ export function DocBrowserFrameContent({
86
87
  ref={iframeRef}
87
88
  key={iframeInstanceId}
88
89
  src={currentUrl}
90
+ name={createUiContentParamsWindowName(currentTab?.contentParams)}
89
91
  className="absolute inset-0 w-full h-full border-0"
90
92
  title={currentTab?.title || 'NextClaw Docs'}
91
93
  sandbox={iframeSandbox}