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.
- package/dist/codemods/v0-12/assistant-api-to-aui.js +1 -0
- package/dist/codemods/v0-12/assistant-api-to-aui.js.map +1 -1
- package/dist/codemods/v0-12/primitive-if-to-aui-if.d.ts.map +1 -1
- package/dist/codemods/v0-12/primitive-if-to-aui-if.js +7 -5
- package/dist/codemods/v0-12/primitive-if-to-aui-if.js.map +1 -1
- package/dist/commands/add.d.ts +0 -1
- package/dist/commands/add.d.ts.map +1 -1
- package/dist/commands/add.js +0 -2
- package/dist/commands/add.js.map +1 -1
- package/dist/commands/create.d.ts +9 -1
- package/dist/commands/create.d.ts.map +1 -1
- package/dist/commands/create.js +20 -6
- package/dist/commands/create.js.map +1 -1
- package/dist/commands/init.js +1 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/mcp.d.ts.map +1 -1
- package/dist/commands/mcp.js +31 -16
- package/dist/commands/mcp.js.map +1 -1
- package/dist/lib/create-project.d.ts +10 -1
- package/dist/lib/create-project.d.ts.map +1 -1
- package/dist/lib/create-project.js +114 -5
- package/dist/lib/create-project.js.map +1 -1
- package/dist/lib/transform.js +1 -1
- package/dist/lib/transform.js.map +1 -1
- package/package.json +4 -4
- package/plugin/skills/assistant-ui/SKILL.md +1 -1
- package/src/codemods/v0-12/__tests__/primitive-if-to-aui-if.test.ts +48 -1
- package/src/codemods/v0-12/assistant-api-to-aui.ts +4 -1
- package/src/codemods/v0-12/primitive-if-to-aui-if.ts +18 -7
- package/src/commands/add.ts +0 -3
- package/src/commands/create.ts +43 -5
- package/src/commands/init.ts +1 -1
- package/src/commands/mcp.ts +37 -12
- package/src/lib/create-project.test.ts +175 -0
- package/src/lib/create-project.ts +170 -6
- package/src/lib/transform.test.ts +25 -0
- 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
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
package/src/commands/add.ts
CHANGED
|
@@ -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
|
});
|
package/src/commands/create.ts
CHANGED
|
@@ -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 ${
|
|
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
|
-
`${
|
|
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 ${
|
|
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(`
|
|
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(`
|
|
799
|
+
logger.info(` ${cdCommand}`);
|
|
762
800
|
if (opts.skipInstall) {
|
|
763
801
|
logger.info(` ${pm} install`);
|
|
764
802
|
}
|
package/src/commands/init.ts
CHANGED
|
@@ -84,7 +84,7 @@ export const init = new Command()
|
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
const createArgs: string[] = [];
|
|
87
|
-
if (projectDirectory) createArgs.push(
|
|
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");
|
package/src/commands/mcp.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
61
|
-
|
|
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("
|
|
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
|
+
});
|