assistant-ui 0.0.113 → 0.0.115

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 (37) hide show
  1. package/dist/codemods/v0-12/assistant-api-to-aui.js +1 -0
  2. package/dist/codemods/v0-12/assistant-api-to-aui.js.map +1 -1
  3. package/dist/codemods/v0-12/primitive-if-to-aui-if.d.ts.map +1 -1
  4. package/dist/codemods/v0-12/primitive-if-to-aui-if.js +7 -5
  5. package/dist/codemods/v0-12/primitive-if-to-aui-if.js.map +1 -1
  6. package/dist/commands/add.d.ts +0 -1
  7. package/dist/commands/add.d.ts.map +1 -1
  8. package/dist/commands/add.js +0 -2
  9. package/dist/commands/add.js.map +1 -1
  10. package/dist/commands/create.d.ts +9 -1
  11. package/dist/commands/create.d.ts.map +1 -1
  12. package/dist/commands/create.js +20 -6
  13. package/dist/commands/create.js.map +1 -1
  14. package/dist/commands/init.js +1 -1
  15. package/dist/commands/init.js.map +1 -1
  16. package/dist/commands/mcp.d.ts.map +1 -1
  17. package/dist/commands/mcp.js +31 -16
  18. package/dist/commands/mcp.js.map +1 -1
  19. package/dist/lib/create-project.d.ts +10 -1
  20. package/dist/lib/create-project.d.ts.map +1 -1
  21. package/dist/lib/create-project.js +114 -5
  22. package/dist/lib/create-project.js.map +1 -1
  23. package/dist/lib/transform.js +1 -1
  24. package/dist/lib/transform.js.map +1 -1
  25. package/package.json +4 -4
  26. package/plugin/skills/assistant-ui/SKILL.md +1 -1
  27. package/src/codemods/v0-12/__tests__/primitive-if-to-aui-if.test.ts +48 -1
  28. package/src/codemods/v0-12/assistant-api-to-aui.ts +4 -1
  29. package/src/codemods/v0-12/primitive-if-to-aui-if.ts +18 -7
  30. package/src/commands/add.ts +0 -3
  31. package/src/commands/create.ts +43 -5
  32. package/src/commands/init.ts +1 -1
  33. package/src/commands/mcp.ts +37 -12
  34. package/src/lib/create-project.test.ts +175 -0
  35. package/src/lib/create-project.ts +170 -6
  36. package/src/lib/transform.test.ts +25 -0
  37. package/src/lib/transform.ts +1 -1
@@ -346,7 +346,7 @@ import { MessagePrimitive, AuiIf } from "@assistant-ui/react";
346
346
 
347
347
  function MyComponent() {
348
348
  return (
349
- <AuiIf condition={(s) => !s.message.speech != null}>
349
+ <AuiIf condition={(s) => !(s.message.speech != null)}>
350
350
  <SpeakIcon />
351
351
  </AuiIf>
352
352
  );
@@ -937,3 +937,50 @@ function MyComponent() {
937
937
  );
938
938
  });
939
939
  });
940
+
941
+ describe("condition precedence", () => {
942
+ const compileCondition = (jsx: string) => {
943
+ const output = applyTransform(`
944
+ import { MessagePrimitive, ComposerPrimitive } from "@assistant-ui/react";
945
+
946
+ const view = ${jsx};
947
+ `);
948
+ const arrow = j(output!).find(j.ArrowFunctionExpression).nodes()[0]!;
949
+ return new Function("s", `return ${j(arrow.body).toSource()};`) as (
950
+ s: unknown,
951
+ ) => boolean;
952
+ };
953
+
954
+ it.each([
955
+ {
956
+ jsx: "<MessagePrimitive.If speaking={false} />",
957
+ hidden: { message: { speech: { status: "running" } } },
958
+ visible: { message: { speech: null } },
959
+ },
960
+ {
961
+ jsx: "<ComposerPrimitive.If dictation={false} />",
962
+ hidden: { composer: { dictation: { status: "running" } } },
963
+ visible: { composer: { dictation: null } },
964
+ },
965
+ {
966
+ jsx: "<MessagePrimitive.If hasContent={false} />",
967
+ hidden: { message: { parts: [{}] } },
968
+ visible: { message: { parts: [] } },
969
+ },
970
+ {
971
+ jsx: "<MessagePrimitive.If lastOrHover copied />",
972
+ hidden: { message: { isHovering: true, isLast: false, isCopied: false } },
973
+ visible: { message: { isHovering: true, isLast: false, isCopied: true } },
974
+ },
975
+ {
976
+ jsx: "<MessagePrimitive.If hasAttachments={false} copied />",
977
+ hidden: { message: { role: "assistant", isCopied: false } },
978
+ visible: { message: { role: "assistant", isCopied: true } },
979
+ },
980
+ ])("keeps every filter in $jsx", ({ jsx, hidden, visible }) => {
981
+ const condition = compileCondition(jsx);
982
+
983
+ expect(condition(hidden)).toBe(false);
984
+ expect(condition(visible)).toBe(true);
985
+ });
986
+ });
@@ -113,6 +113,7 @@ const migrateAssistantApiToAui = createTransformer(
113
113
  }
114
114
  if (j.VariableDeclaration.check(statement)) {
115
115
  for (const declarator of statement.declarations) {
116
+ if (!j.VariableDeclarator.check(declarator)) continue;
116
117
  if (
117
118
  j.Identifier.check(declarator.id) &&
118
119
  declarator.id.name === "api"
@@ -252,9 +253,11 @@ const migrateAssistantApiToAui = createTransformer(
252
253
  grandparent.source != null
253
254
  )
254
255
  return;
256
+ // Babel emits exportKind on ExportSpecifier for inline
257
+ // `export { type api }`; ast-types' typings omit it.
255
258
  if (
256
259
  grandparent?.exportKind === "type" ||
257
- parent.exportKind === "type"
260
+ (parent as { exportKind?: string }).exportKind === "type"
258
261
  )
259
262
  return;
260
263
  if (parent.exported === path.value && parent.local !== path.value)
@@ -161,12 +161,23 @@ const getAttrValue = (j: any, attr: any): unknown => {
161
161
  return UNSUPPORTED_VALUE;
162
162
  };
163
163
 
164
- const buildConditionString = (fragments: ConditionFragment[]): string => {
165
- const parts = fragments.map((f) =>
166
- f.negated ? `!${f.expression}` : f.expression,
167
- );
168
- if (parts.length === 1) return parts[0]!;
169
- return parts.join(" && ");
164
+ // Composed as a syntax tree so the printer parenthesizes by precedence;
165
+ // concatenated text lets `!` and `&&` reassociate a fragment's own operators.
166
+ const buildConditionString = (
167
+ j: any,
168
+ fragments: ConditionFragment[],
169
+ ): string => {
170
+ const parseExpression = (source: string) =>
171
+ j(`${source};`).find(j.ExpressionStatement).nodes()[0]!.expression;
172
+
173
+ const condition = fragments
174
+ .map((f) => {
175
+ const expression = parseExpression(f.expression);
176
+ return f.negated ? j.unaryExpression("!", expression) : expression;
177
+ })
178
+ .reduce((left: any, right: any) => j.logicalExpression("&&", left, right));
179
+
180
+ return j(condition).toSource();
170
181
  };
171
182
 
172
183
  const migratePrimitiveIfToAuiIf = createTransformer(
@@ -290,7 +301,7 @@ const migratePrimitiveIfToAuiIf = createTransformer(
290
301
  // If we couldn't map all props, skip this element
291
302
  if (hasUnknownProp || fragments.length === 0) return;
292
303
 
293
- convertElementToAuiIf(path, buildConditionString(fragments));
304
+ convertElementToAuiIf(path, buildConditionString(j, fragments));
294
305
  });
295
306
 
296
307
  // Add AuiIf import if needed
@@ -22,7 +22,6 @@ export function createAddComponentsPlan(params: {
22
22
  packageManager: PackageManagerName;
23
23
  yes?: boolean;
24
24
  overwrite?: boolean;
25
- cwd?: string;
26
25
  path?: string;
27
26
  style?: string;
28
27
  }): AddComponentsPlan {
@@ -40,7 +39,6 @@ export function createAddComponentsPlan(params: {
40
39
  // This flag is for shadcn's own confirmation prompt.
41
40
  if (params.yes) args.push("--yes");
42
41
  if (params.overwrite) args.push("--overwrite");
43
- if (params.cwd) args.push("--cwd", params.cwd);
44
42
  if (params.path) args.push("--path", params.path);
45
43
 
46
44
  return { command, args };
@@ -83,7 +81,6 @@ export const add = new Command()
83
81
  packageManager,
84
82
  yes: opts.yes,
85
83
  overwrite: opts.overwrite,
86
- cwd: opts.cwd,
87
84
  path: opts.path,
88
85
  ...(style === undefined ? {} : { style }),
89
86
  });
@@ -388,6 +388,42 @@ export function resolveCreateProjectDirectory(params: {
388
388
  return undefined;
389
389
  }
390
390
 
391
+ export function resolveProjectDirectoryGuidance(params: {
392
+ absoluteProjectDir: string;
393
+ cwd?: string;
394
+ platform?: NodeJS.Platform;
395
+ }): { display: string; cdCommand: string } {
396
+ const {
397
+ absoluteProjectDir,
398
+ cwd = process.cwd(),
399
+ platform = process.platform,
400
+ } = params;
401
+ const isWindows = platform === "win32";
402
+ const pathApi = isWindows ? path.win32 : path.posix;
403
+
404
+ const relative = pathApi.relative(cwd, absoluteProjectDir);
405
+ const escapesCwd =
406
+ relative === ".." || relative.startsWith(`..${pathApi.sep}`);
407
+ const display =
408
+ relative && !escapesCwd && !pathApi.isAbsolute(relative)
409
+ ? relative
410
+ : absoluteProjectDir;
411
+
412
+ const target = display.startsWith("-")
413
+ ? `.${pathApi.sep}${display}`
414
+ : display;
415
+ // Neither Windows shell has a literal quoting form the other accepts: cmd
416
+ // reads single quotes as part of the name, and double quotes still expand
417
+ // %VAR% there and $var in PowerShell.
418
+ const quoted = (isWindows ? /^[\w@.:/\\+-]+$/ : /^[\w@./+-]+$/).test(target)
419
+ ? target
420
+ : isWindows
421
+ ? `"${target}"`
422
+ : `'${target.replaceAll("'", "'\\''")}'`;
423
+
424
+ return { display, cdCommand: `cd ${quoted}` };
425
+ }
426
+
391
427
  const PLAYGROUND_PRESET_BASE_URL =
392
428
  "https://www.assistant-ui.com/playground/init";
393
429
 
@@ -542,11 +578,13 @@ export const create = new Command()
542
578
 
543
579
  // Check directory
544
580
  const absoluteProjectDir = path.resolve(resolvedProjectDirectory);
581
+ const { display: displayProjectDir, cdCommand } =
582
+ resolveProjectDirectoryGuidance({ absoluteProjectDir });
545
583
  try {
546
584
  const files = fs.readdirSync(absoluteProjectDir);
547
585
  if (files.length > 0) {
548
586
  logger.error(
549
- `Directory ${resolvedProjectDirectory} already exists and is not empty`,
587
+ `Directory ${displayProjectDir} already exists and is not empty`,
550
588
  );
551
589
  process.exit(1);
552
590
  }
@@ -557,12 +595,12 @@ export const create = new Command()
557
595
  // Directory doesn't exist — good, proceed
558
596
  } else if (code === "ENOTDIR") {
559
597
  logger.error(
560
- `${resolvedProjectDirectory} already exists and is not a directory`,
598
+ `${displayProjectDir} already exists and is not a directory`,
561
599
  );
562
600
  process.exit(1);
563
601
  } else {
564
602
  const message = err instanceof Error ? err.message : String(err);
565
- logger.error(`Cannot access ${resolvedProjectDirectory}: ${message}`);
603
+ logger.error(`Cannot access ${displayProjectDir}: ${message}`);
566
604
  process.exit(1);
567
605
  }
568
606
  }
@@ -700,7 +738,7 @@ export const create = new Command()
700
738
  logger.break();
701
739
  logger.error("Project created with missing components.");
702
740
  logger.info("Retry the component install with:");
703
- logger.info(` cd ${resolvedProjectDirectory}`);
741
+ logger.info(` ${cdCommand}`);
704
742
  logger.info(` ${transformResult.registryInstallFailure.retryCommand}`);
705
743
  process.exit(1);
706
744
  }
@@ -758,7 +796,7 @@ export const create = new Command()
758
796
  }
759
797
 
760
798
  logger.info("Next steps:");
761
- logger.info(` cd ${resolvedProjectDirectory}`);
799
+ logger.info(` ${cdCommand}`);
762
800
  if (opts.skipInstall) {
763
801
  logger.info(` ${pm} install`);
764
802
  }
@@ -84,7 +84,7 @@ export const init = new Command()
84
84
  }
85
85
 
86
86
  const createArgs: string[] = [];
87
- if (projectDirectory) createArgs.push(projectDirectory);
87
+ if (projectDirectory) createArgs.push(targetDir);
88
88
  if (presetUrl) createArgs.push("--preset", presetUrl);
89
89
  if (opts.useNpm) createArgs.push("--use-npm");
90
90
  if (opts.usePnpm) createArgs.push("--use-pnpm");
@@ -14,12 +14,15 @@ type MCPTarget =
14
14
  | "claude-code"
15
15
  | "claude-desktop";
16
16
 
17
+ const HOSTED_MCP_URL = "https://www.assistant-ui.com/mcp";
18
+
17
19
  const MCP_CONFIGS: Record<
18
20
  Exclude<MCPTarget, "claude-code">,
19
21
  {
20
22
  name: string;
21
23
  getPath: () => string;
22
24
  config: object;
25
+ replaceServerKey?: string;
23
26
  postInstall?: string;
24
27
  }
25
28
  > = {
@@ -29,11 +32,11 @@ const MCP_CONFIGS: Record<
29
32
  config: {
30
33
  mcpServers: {
31
34
  "assistant-ui": {
32
- command: "npx",
33
- args: ["-y", "@assistant-ui/mcp-docs-server"],
35
+ url: HOSTED_MCP_URL,
34
36
  },
35
37
  },
36
38
  },
39
+ replaceServerKey: "mcpServers",
37
40
  postInstall:
38
41
  "Open Cursor Settings → MCP → find 'assistant-ui' and click enable.",
39
42
  },
@@ -44,11 +47,11 @@ const MCP_CONFIGS: Record<
44
47
  config: {
45
48
  mcpServers: {
46
49
  "assistant-ui": {
47
- command: "npx",
48
- args: ["-y", "@assistant-ui/mcp-docs-server"],
50
+ serverUrl: HOSTED_MCP_URL,
49
51
  },
50
52
  },
51
53
  },
54
+ replaceServerKey: "mcpServers",
52
55
  postInstall: "Fully quit and re-open Windsurf to activate.",
53
56
  },
54
57
  vscode: {
@@ -57,12 +60,12 @@ const MCP_CONFIGS: Record<
57
60
  config: {
58
61
  servers: {
59
62
  "assistant-ui": {
60
- command: "npx",
61
- args: ["-y", "@assistant-ui/mcp-docs-server"],
62
- type: "stdio",
63
+ type: "http",
64
+ url: HOSTED_MCP_URL,
63
65
  },
64
66
  },
65
67
  },
68
+ replaceServerKey: "servers",
66
69
  postInstall:
67
70
  "Enable MCP in Settings → search 'MCP' → enable 'Chat > MCP'. Use Copilot Chat in Agent mode.",
68
71
  },
@@ -153,14 +156,18 @@ async function installForTarget(target: MCPTarget): Promise<void> {
153
156
  logger.break();
154
157
 
155
158
  try {
159
+ await runSpawn("claude", ["mcp", "remove", "assistant-ui"]).catch(
160
+ (error: unknown) => {
161
+ if (error instanceof SpawnSignalError) throw error;
162
+ },
163
+ );
156
164
  await runSpawn("claude", [
157
165
  "mcp",
158
166
  "add",
167
+ "--transport",
168
+ "http",
159
169
  "assistant-ui",
160
- "--",
161
- "npx",
162
- "-y",
163
- "@assistant-ui/mcp-docs-server",
170
+ HOSTED_MCP_URL,
164
171
  ]);
165
172
  } catch (error) {
166
173
  if (error instanceof SpawnSignalError) throw error;
@@ -177,6 +184,9 @@ async function installForTarget(target: MCPTarget): Promise<void> {
177
184
 
178
185
  logger.break();
179
186
  logger.success("MCP server installed for Claude Code!");
187
+ logger.info(
188
+ `Connects to the hosted assistant-ui MCP server at ${HOSTED_MCP_URL}.`,
189
+ );
180
190
  logger.info(
181
191
  "The server starts automatically. Try asking about assistant-ui!",
182
192
  );
@@ -220,12 +230,27 @@ async function installForTarget(target: MCPTarget): Promise<void> {
220
230
 
221
231
  const newConfig = deepMerge(existingConfig, targetConfig.config);
222
232
 
233
+ if (targetConfig.replaceServerKey) {
234
+ const key = targetConfig.replaceServerKey;
235
+ newConfig[key] = {
236
+ ...newConfig[key],
237
+ "assistant-ui": (targetConfig.config as any)[key]["assistant-ui"],
238
+ };
239
+ }
240
+
223
241
  fs.writeFileSync(configPath, `${JSON.stringify(newConfig, null, 2)}\n`);
224
242
 
225
243
  logger.break();
226
244
  logger.success(`MCP server installed for ${targetConfig.name}!`);
227
245
  logger.info(`Config written to: ${configPath}`);
228
246
 
247
+ if (targetConfig.replaceServerKey) {
248
+ logger.break();
249
+ logger.info(
250
+ `Connects to the hosted assistant-ui MCP server at ${HOSTED_MCP_URL}.`,
251
+ );
252
+ }
253
+
229
254
  if (targetConfig.postInstall) {
230
255
  logger.break();
231
256
  logger.info(targetConfig.postInstall);
@@ -234,7 +259,7 @@ async function installForTarget(target: MCPTarget): Promise<void> {
234
259
 
235
260
  export const mcp = new Command()
236
261
  .name("mcp")
237
- .description("install assistant-ui MCP docs server for your IDE")
262
+ .description("connect your IDE to the assistant-ui MCP server")
238
263
  .option("--cursor", "install for Cursor")
239
264
  .option("--windsurf", "install for Windsurf")
240
265
  .option("--vscode", "install for VSCode")
@@ -0,0 +1,175 @@
1
+ import * as fs from "node:fs";
2
+ import * as os from "node:os";
3
+ import * as path from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+ import { reconcileAssistantUIImportLayout } from "./create-project";
6
+
7
+ describe("reconcileAssistantUIImportLayout", () => {
8
+ let projectDir: string;
9
+
10
+ beforeEach(() => {
11
+ projectDir = fs.mkdtempSync(path.join(os.tmpdir(), "aui-cli-test-"));
12
+ });
13
+
14
+ afterEach(() => {
15
+ fs.rmSync(projectDir, { recursive: true, force: true });
16
+ });
17
+
18
+ const write = (file: string, content: string) => {
19
+ const fullPath = path.join(projectDir, file);
20
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
21
+ fs.writeFileSync(fullPath, content);
22
+ };
23
+
24
+ const read = (file: string) =>
25
+ fs.readFileSync(path.join(projectDir, file), "utf8");
26
+
27
+ it("rewrites a legacy import when only the elements layout exists", async () => {
28
+ write(
29
+ "app/page.tsx",
30
+ 'import { Thread } from "@/components/assistant-ui/thread";\n' +
31
+ 'import { ThreadList } from "@/components/assistant-ui/thread-list";\n',
32
+ );
33
+ write("components/assistant-ui/elements/thread.aui.tsx", "export {};");
34
+ write("components/assistant-ui/elements/thread-list.aui.tsx", "export {};");
35
+
36
+ await reconcileAssistantUIImportLayout(projectDir);
37
+
38
+ expect(read("app/page.tsx")).toBe(
39
+ 'import { Thread } from "@/components/assistant-ui/elements/thread.aui";\n' +
40
+ 'import { ThreadList } from "@/components/assistant-ui/elements/thread-list.aui";\n',
41
+ );
42
+ });
43
+
44
+ it("rewrites module declarations without changing import-like source text", async () => {
45
+ write(
46
+ "app/page.tsx",
47
+ 'import { Thread } from "@/components/assistant-ui/thread";\n' +
48
+ 'export { ThreadList } from "@/components/assistant-ui/thread-list";\n' +
49
+ "const example = 'from \"@/components/assistant-ui/thread\"';\n" +
50
+ '// from "@/components/assistant-ui/thread-list"\n',
51
+ );
52
+ write("components/assistant-ui/elements/thread.aui.tsx", "export {};");
53
+ write("components/assistant-ui/elements/thread-list.aui.tsx", "export {};");
54
+
55
+ await reconcileAssistantUIImportLayout(projectDir);
56
+
57
+ expect(read("app/page.tsx")).toBe(
58
+ 'import { Thread } from "@/components/assistant-ui/elements/thread.aui";\n' +
59
+ 'export { ThreadList } from "@/components/assistant-ui/elements/thread-list.aui";\n' +
60
+ "const example = 'from \"@/components/assistant-ui/thread\"';\n" +
61
+ '// from "@/components/assistant-ui/thread-list"\n',
62
+ );
63
+ });
64
+
65
+ it("rewrites a legacy import to a bare elements file without the .aui segment", async () => {
66
+ write(
67
+ "app/MyThread.tsx",
68
+ 'import { MarkdownText } from "@/components/assistant-ui/markdown-text";\n' +
69
+ 'import { TooltipIconButton } from "@/components/assistant-ui/tooltip-icon-button";\n',
70
+ );
71
+ write("components/assistant-ui/elements/markdown-text.tsx", "export {};");
72
+ write(
73
+ "components/assistant-ui/elements/tooltip-icon-button.tsx",
74
+ "export {};",
75
+ );
76
+
77
+ await reconcileAssistantUIImportLayout(projectDir);
78
+
79
+ expect(read("app/MyThread.tsx")).toBe(
80
+ 'import { MarkdownText } from "@/components/assistant-ui/elements/markdown-text";\n' +
81
+ 'import { TooltipIconButton } from "@/components/assistant-ui/elements/tooltip-icon-button";\n',
82
+ );
83
+ });
84
+
85
+ it("prefers the .aui variant when both variants share a basename", async () => {
86
+ write(
87
+ "app/page.tsx",
88
+ 'import { Reasoning } from "@/components/assistant-ui/reasoning";\n',
89
+ );
90
+ write("components/assistant-ui/elements/reasoning.tsx", "export {};");
91
+ write("components/assistant-ui/elements/reasoning.aui.tsx", "export {};");
92
+
93
+ await reconcileAssistantUIImportLayout(projectDir);
94
+
95
+ expect(read("app/page.tsx")).toBe(
96
+ 'import { Reasoning } from "@/components/assistant-ui/elements/reasoning.aui";\n',
97
+ );
98
+ });
99
+
100
+ it("supports the src/ project layout", async () => {
101
+ write(
102
+ "src/routes/index.tsx",
103
+ 'import { Thread } from "@/components/assistant-ui/thread";\n',
104
+ );
105
+ write("src/components/assistant-ui/elements/thread.aui.tsx", "export {};");
106
+
107
+ await reconcileAssistantUIImportLayout(projectDir);
108
+
109
+ expect(read("src/routes/index.tsx")).toBe(
110
+ 'import { Thread } from "@/components/assistant-ui/elements/thread.aui";\n',
111
+ );
112
+ });
113
+
114
+ it("leaves imports alone when the legacy file exists", async () => {
115
+ const source =
116
+ 'import { Thread } from "@/components/assistant-ui/thread";\n';
117
+ write("app/page.tsx", source);
118
+ write("components/assistant-ui/thread.tsx", "export {};");
119
+ write("components/assistant-ui/elements/thread.aui.tsx", "export {};");
120
+
121
+ await reconcileAssistantUIImportLayout(projectDir);
122
+
123
+ expect(read("app/page.tsx")).toBe(source);
124
+ });
125
+
126
+ it("leaves imports alone when neither layout has the file", async () => {
127
+ const source =
128
+ 'import { Custom } from "@/components/assistant-ui/custom-part";\n';
129
+ write("app/page.tsx", source);
130
+ write("components/assistant-ui/elements/thread.aui.tsx", "export {};");
131
+
132
+ await reconcileAssistantUIImportLayout(projectDir);
133
+
134
+ expect(read("app/page.tsx")).toBe(source);
135
+ });
136
+
137
+ it("leaves elements imports untouched", async () => {
138
+ const source =
139
+ 'import { Thread } from "@/components/assistant-ui/elements/thread.aui";\n';
140
+ write("app/page.tsx", source);
141
+ write("components/assistant-ui/elements/thread.aui.tsx", "export {};");
142
+
143
+ await reconcileAssistantUIImportLayout(projectDir);
144
+
145
+ expect(read("app/page.tsx")).toBe(source);
146
+ });
147
+
148
+ it("parses TypeScript files with generic arrow functions", async () => {
149
+ write(
150
+ "app/helpers.ts",
151
+ 'import { Thread } from "@/components/assistant-ui/thread";\n' +
152
+ "export const identity = <T>(value: T) => value;\n",
153
+ );
154
+ write("components/assistant-ui/elements/thread.aui.tsx", "export {};");
155
+
156
+ await reconcileAssistantUIImportLayout(projectDir);
157
+
158
+ expect(read("app/helpers.ts")).toBe(
159
+ 'import { Thread } from "@/components/assistant-ui/elements/thread.aui";\n' +
160
+ "export const identity = <T>(value: T) => value;\n",
161
+ );
162
+ });
163
+
164
+ it("leaves unparseable source files untouched", async () => {
165
+ const source =
166
+ 'import { Thread } from "@/components/assistant-ui/thread";\nconst broken = ;\n';
167
+ write("app/broken.ts", source);
168
+ write("components/assistant-ui/elements/thread.aui.tsx", "export {};");
169
+
170
+ await expect(
171
+ reconcileAssistantUIImportLayout(projectDir),
172
+ ).resolves.toBeUndefined();
173
+ expect(read("app/broken.ts")).toBe(source);
174
+ });
175
+ });