@elizaos/app-core 2.0.0-alpha.13 → 2.0.0-alpha.15

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 (125) hide show
  1. package/.turbo/turbo-build.log +0 -1
  2. package/dist/App.d.ts.map +1 -1
  3. package/dist/App.js +26 -12
  4. package/dist/api/client.d.ts +3 -3
  5. package/dist/api/client.d.ts.map +1 -1
  6. package/dist/api/client.js +24 -13
  7. package/dist/bridge/plugin-bridge.d.ts.map +1 -1
  8. package/dist/components/AvatarLoader.d.ts +3 -1
  9. package/dist/components/AvatarLoader.d.ts.map +1 -1
  10. package/dist/components/AvatarLoader.js +4 -1
  11. package/dist/components/BscTradePanel.d.ts +1 -1
  12. package/dist/components/BscTradePanel.d.ts.map +1 -1
  13. package/dist/components/CharacterView.d.ts.map +1 -1
  14. package/dist/components/CharacterView.js +50 -15
  15. package/dist/components/ChatView.js +1 -1
  16. package/dist/components/ConfigPageView.d.ts.map +1 -1
  17. package/dist/components/ConfigPageView.js +9 -8
  18. package/dist/components/ConversationsSidebar.js +1 -1
  19. package/dist/components/CustomActionEditor.js +1 -1
  20. package/dist/components/FineTuningView.d.ts.map +1 -1
  21. package/dist/components/FineTuningView.js +2 -2
  22. package/dist/components/GlobalEmoteOverlay.d.ts.map +1 -1
  23. package/dist/components/GlobalEmoteOverlay.js +1 -1
  24. package/dist/components/HeartbeatsView.js +1 -1
  25. package/dist/components/LoadingScreen.d.ts.map +1 -1
  26. package/dist/components/LoadingScreen.js +38 -7
  27. package/dist/components/OnboardingWizard.js +1 -1
  28. package/dist/components/PluginsView.d.ts.map +1 -1
  29. package/dist/components/PluginsView.js +4 -1
  30. package/dist/components/ShellOverlays.js +1 -1
  31. package/dist/components/VoiceConfigView.d.ts.map +1 -1
  32. package/dist/components/VoiceConfigView.js +4 -2
  33. package/dist/components/avatar/VrmEngine.d.ts +2 -0
  34. package/dist/components/avatar/VrmEngine.d.ts.map +1 -1
  35. package/dist/components/avatar/VrmEngine.js +14 -6
  36. package/dist/components/companion/CompanionSceneHost.d.ts +1 -1
  37. package/dist/components/companion/CompanionSceneHost.d.ts.map +1 -1
  38. package/dist/components/companion/CompanionSceneHost.js +1 -1
  39. package/dist/components/index.d.ts +10 -10
  40. package/dist/components/index.d.ts.map +1 -1
  41. package/dist/components/index.js +10 -10
  42. package/dist/components/inventory/TokensTable.js +2 -2
  43. package/dist/components/onboarding/IdentityStep.d.ts.map +1 -1
  44. package/dist/components/onboarding/IdentityStep.js +1 -1
  45. package/dist/components/shared/ShellHeaderControls.js +1 -1
  46. package/dist/config/config-field.d.ts.map +1 -1
  47. package/dist/config/config-field.js +7 -8
  48. package/dist/config/index.d.ts +1 -1
  49. package/dist/config/index.d.ts.map +1 -1
  50. package/dist/config/index.js +1 -1
  51. package/dist/hooks/useVoiceChat.d.ts.map +1 -1
  52. package/dist/hooks/useVoiceChat.js +3 -1
  53. package/dist/i18n/locales/en.json +1192 -1192
  54. package/dist/i18n/locales/es.json +1192 -1192
  55. package/dist/i18n/locales/ko.json +1192 -1192
  56. package/dist/i18n/locales/pt.json +1192 -1192
  57. package/dist/i18n/locales/zh-CN.json +1192 -1192
  58. package/dist/package.json +181 -0
  59. package/dist/platform/lifo.d.ts.map +1 -1
  60. package/dist/platform/lifo.js +4 -1
  61. package/dist/state/AppContext.d.ts.map +1 -1
  62. package/dist/state/AppContext.js +23 -6
  63. package/dist/state/internal.d.ts +1 -1
  64. package/dist/state/internal.d.ts.map +1 -1
  65. package/dist/state/internal.js +1 -1
  66. package/dist/state/parsers.d.ts.map +1 -1
  67. package/dist/state/parsers.js +3 -2
  68. package/dist/state/persistence.js +1 -1
  69. package/dist/styles/anime.css +6324 -0
  70. package/dist/styles/base.css +196 -0
  71. package/dist/styles/onboarding-game.css +738 -0
  72. package/dist/styles/styles.css +2087 -0
  73. package/dist/styles/xterm.css +241 -0
  74. package/package.json +4 -4
  75. package/src/App.tsx +35 -14
  76. package/src/ambient.d.ts +5 -5
  77. package/src/api/client.ts +36 -23
  78. package/src/bridge/plugin-bridge.ts +1 -1
  79. package/src/components/AvatarLoader.tsx +6 -0
  80. package/src/components/BscTradePanel.tsx +1 -1
  81. package/src/components/CharacterView.tsx +536 -367
  82. package/src/components/ChatView.tsx +3 -3
  83. package/src/components/ConfigPageView.tsx +9 -8
  84. package/src/components/ConversationsSidebar.tsx +1 -1
  85. package/src/components/CustomActionEditor.tsx +6 -6
  86. package/src/components/FineTuningView.tsx +6 -3
  87. package/src/components/GlobalEmoteOverlay.tsx +1 -4
  88. package/src/components/HeartbeatsView.tsx +1 -1
  89. package/src/components/InventoryView.tsx +2 -2
  90. package/src/components/LoadingScreen.tsx +39 -6
  91. package/src/components/OnboardingWizard.tsx +1 -1
  92. package/src/components/PluginsView.tsx +6 -0
  93. package/src/components/ShellOverlays.tsx +1 -1
  94. package/src/components/VoiceConfigView.tsx +4 -5
  95. package/src/components/avatar/VrmEngine.ts +25 -7
  96. package/src/components/companion/CompanionSceneHost.tsx +5 -1
  97. package/src/components/index.ts +10 -10
  98. package/src/components/inventory/TokensTable.tsx +2 -2
  99. package/src/components/onboarding/IdentityStep.tsx +9 -13
  100. package/src/components/shared/ShellHeaderControls.tsx +1 -1
  101. package/src/config/config-field.tsx +7 -8
  102. package/src/config/index.ts +3 -3
  103. package/src/hooks/useVoiceChat.ts +5 -3
  104. package/src/platform/lifo.ts +14 -4
  105. package/src/state/AppContext.tsx +24 -1
  106. package/src/state/internal.ts +6 -6
  107. package/src/state/parsers.ts +4 -3
  108. package/src/state/persistence.ts +1 -1
  109. package/test/app/MessageContent.test.tsx +42 -0
  110. package/test/app/bug-report-modal.test.tsx +3 -3
  111. package/test/app/chat-view.test.tsx +3 -3
  112. package/test/app/cloud-login-lock.test.ts +3 -2
  113. package/test/app/custom-actions-smoke.test.ts +3 -3
  114. package/test/app/onboarding-language.test.tsx +3 -3
  115. package/test/app/pages-navigation-smoke.e2e.test.ts +13 -8
  116. package/test/app/plugin-bridge.test.ts +1 -1
  117. package/test/app/provider-dropdown-default.test.tsx +2 -4
  118. package/test/app/restart-banner.test.tsx +3 -3
  119. package/test/app/shell-mode-switching.e2e.test.ts +6 -6
  120. package/test/app/shell-mode-tab-memory.test.tsx +1 -1
  121. package/test/app/startup-chat.e2e.test.ts +3 -3
  122. package/test/app/triggers-view.e2e.test.ts +3 -2
  123. package/test/app/wallet-api-save-lock.test.ts +2 -1
  124. package/test/utils/assistant-text.test.ts +64 -0
  125. package/test/utils/streaming-text.test.ts +89 -0
@@ -77,7 +77,12 @@ function parseLocationParams(locationLike: LocationLike): {
77
77
  export function isLifoPopoutValue(value: string | null | undefined): boolean {
78
78
  if (typeof value !== "string") return false;
79
79
  const normalized = value.trim().toLowerCase();
80
- return normalized === "" || normalized === "1" || normalized === "true" || normalized === "lifo";
80
+ return (
81
+ normalized === "" ||
82
+ normalized === "1" ||
83
+ normalized === "true" ||
84
+ normalized === "lifo"
85
+ );
81
86
  }
82
87
 
83
88
  export function getPopoutValueFromLocation(
@@ -87,7 +92,9 @@ export function getPopoutValueFromLocation(
87
92
  return search.get("popout") ?? hash.get("popout");
88
93
  }
89
94
 
90
- export function isLifoPopoutModeAtLocation(locationLike: LocationLike): boolean {
95
+ export function isLifoPopoutModeAtLocation(
96
+ locationLike: LocationLike,
97
+ ): boolean {
91
98
  return isLifoPopoutValue(getPopoutValueFromLocation(locationLike));
92
99
  }
93
100
 
@@ -121,7 +128,9 @@ export function generateLifoSessionId(): string {
121
128
  bytes[index] = Math.floor(Math.random() * 256);
122
129
  }
123
130
  }
124
- return Array.from(bytes, (value) => value.toString(16).padStart(2, "0")).join("");
131
+ return Array.from(bytes, (value) => value.toString(16).padStart(2, "0")).join(
132
+ "",
133
+ );
125
134
  }
126
135
 
127
136
  export function getLifoSyncChannelName(sessionId: string | null): string {
@@ -205,7 +214,8 @@ export async function createLifoRuntime(
205
214
  options?.onStdout?.("Command dispatched to sandbox runtime.\n");
206
215
  return { exitCode: 0 };
207
216
  } catch (error) {
208
- const message = error instanceof Error ? error.message : String(error);
217
+ const message =
218
+ error instanceof Error ? error.message : String(error);
209
219
  options?.onStderr?.(`${message}\n`);
210
220
  throw error;
211
221
  }
@@ -4,6 +4,7 @@
4
4
  * Children access state and actions through the useApp() hook.
5
5
  */
6
6
 
7
+ import type { OnboardingConnection } from "@elizaos/autonomous/contracts/onboarding";
7
8
  import {
8
9
  type ReactNode,
9
10
  useCallback,
@@ -12,7 +13,6 @@ import {
12
13
  useRef,
13
14
  useState,
14
15
  } from "react";
15
- import type { OnboardingConnection } from "@elizaos/autonomous/contracts/onboarding";
16
16
  import { prepareDraftForSave } from "../actions/character";
17
17
  import {
18
18
  type AgentStartupDiagnostics,
@@ -926,6 +926,8 @@ export function AppProvider({ children }: { children: ReactNode }) {
926
926
 
927
927
  // --- Refs for timers ---
928
928
  const actionNoticeTimer = useRef<number | null>(null);
929
+ /** Session-scoped set of notice texts that have been shown with once=true. */
930
+ const shownOnceNotices = useRef<Set<string>>(new Set());
929
931
  const elizaCloudPollInterval = useRef<number | null>(null);
930
932
  const elizaCloudLoginPollTimer = useRef<number | null>(null);
931
933
  const prevAgentStateRef = useRef<string | null>(null);
@@ -989,7 +991,10 @@ export function AppProvider({ children }: { children: ReactNode }) {
989
991
  text: string,
990
992
  tone: "info" | "success" | "error" = "info",
991
993
  ttlMs = 2800,
994
+ once = false,
992
995
  ) => {
996
+ if (once && shownOnceNotices.current.has(text)) return;
997
+ if (once) shownOnceNotices.current.add(text);
993
998
  setActionNoticeState({ tone, text });
994
999
  if (actionNoticeTimer.current != null) {
995
1000
  window.clearTimeout(actionNoticeTimer.current);
@@ -3092,6 +3097,24 @@ export function AppProvider({ children }: { children: ReactNode }) {
3092
3097
  conversationMessagesRef.current.length > 0
3093
3098
  )
3094
3099
  return;
3100
+
3101
+ // Clean up empty conversations: if the previous conversation has only
3102
+ // system/greeting messages and no user messages, delete it silently.
3103
+ const prevId = activeConversationId;
3104
+ if (prevId && prevId !== id) {
3105
+ const prevMessages = conversationMessagesRef.current;
3106
+ const hasUserMessage = prevMessages.some((m) => m.role === "user");
3107
+ if (!hasUserMessage && prevMessages.length <= 1) {
3108
+ void client.deleteConversation(prevId).catch(() => {});
3109
+ setConversations((prev) => prev.filter((c) => c.id !== prevId));
3110
+ setUnreadConversations((prev) => {
3111
+ const next = new Set(prev);
3112
+ next.delete(prevId);
3113
+ return next;
3114
+ });
3115
+ }
3116
+ }
3117
+
3095
3118
  const previousActive = activeConversationId;
3096
3119
  setActiveConversationId(id);
3097
3120
  activeConversationIdRef.current = id;
@@ -1,3 +1,9 @@
1
+ export {
2
+ deriveOnboardingResumeConnection,
3
+ deriveOnboardingResumeFields,
4
+ hasPartialOnboardingConnectionConfig,
5
+ inferOnboardingResumeStep,
6
+ } from "./onboarding-resume";
1
7
  export {
2
8
  asApiLikeError,
3
9
  computeStreamingDelta,
@@ -15,12 +21,6 @@ export {
15
21
  parseStreamEventEnvelopeEvent,
16
22
  shouldApplyFinalStreamText,
17
23
  } from "./parsers";
18
- export {
19
- deriveOnboardingResumeConnection,
20
- deriveOnboardingResumeFields,
21
- hasPartialOnboardingConnectionConfig,
22
- inferOnboardingResumeStep,
23
- } from "./onboarding-resume";
24
24
  export {
25
25
  applyUiTheme,
26
26
  clearPersistedOnboardingStep,
@@ -171,11 +171,12 @@ function normalizeSlashCommandName(name: string): string {
171
171
 
172
172
  // A simple utility to split command arguments, equivalent to chat-commands splitCommandArgs
173
173
  function splitCommandArgs(text: string): string[] {
174
- const parts = [];
174
+ const parts: string[] = [];
175
175
  const regex = /[^\s"']+|"([^"]*)"|'([^']*)'/g;
176
- let match;
177
- while ((match = regex.exec(text)) !== null) {
176
+ let match: RegExpExecArray | null = regex.exec(text);
177
+ while (match !== null) {
178
178
  parts.push(match[1] || match[2] || match[0]);
179
+ match = regex.exec(text);
179
180
  }
180
181
  return parts;
181
182
  }
@@ -289,7 +289,7 @@ export function loadActiveConversationId(): string | null {
289
289
 
290
290
  export function saveActiveConversationId(value: string | null): void {
291
291
  try {
292
- if (value && value.trim()) {
292
+ if (value?.trim()) {
293
293
  localStorage.setItem(ACTIVE_CONVERSATION_ID_KEY, value);
294
294
  return;
295
295
  }
@@ -324,3 +324,45 @@ describe("MessageContent — XML action stripping", () => {
324
324
  expect(rendered).toBe("**Saturday**: Sunny and warm.");
325
325
  });
326
326
  });
327
+
328
+ describe("XML tag stripping during streaming", () => {
329
+ it("strips <actions> blocks completely", () => {
330
+ const output = renderText(
331
+ 'Hello world<actions><action name="test">do thing</action></actions>',
332
+ );
333
+ expect(output).toContain("Hello world");
334
+ expect(output).not.toContain("do thing");
335
+ expect(output).not.toContain("<actions>");
336
+ });
337
+
338
+ it("strips <think> blocks completely", () => {
339
+ const output = renderText(
340
+ "<think>internal reasoning about the query</think>Hello there!",
341
+ );
342
+ expect(output).toContain("Hello there!");
343
+ expect(output).not.toContain("internal reasoning");
344
+ expect(output).not.toContain("<think>");
345
+ });
346
+
347
+ it("extracts text from <response><text> wrapper", () => {
348
+ const output = renderText(
349
+ "<response><text>Hello world</text></response>",
350
+ );
351
+ expect(output).toContain("Hello world");
352
+ expect(output).not.toContain("<response>");
353
+ expect(output).not.toContain("<text>");
354
+ });
355
+
356
+ it("handles partial <response><text> during streaming (no closing tag yet)", () => {
357
+ const output = renderText("<response><text>Hello wor");
358
+ expect(output).toContain("Hello wor");
359
+ });
360
+
361
+ it("does not leak <actions> tag name as visible text", () => {
362
+ const output = renderText(
363
+ 'Answer here<actions><action name="save">data</action></actions> done',
364
+ );
365
+ expect(output).not.toContain("actions");
366
+ expect(output).not.toContain("save");
367
+ });
368
+ });
@@ -14,9 +14,9 @@ const { mockUseBugReport, mockClient } = vi.hoisted(() => ({
14
14
  }));
15
15
 
16
16
  vi.mock("@elizaos/app-core/hooks", async () => {
17
- const actual = await vi.importActual<typeof import("@elizaos/app-core/hooks")>(
18
- "@elizaos/app-core/hooks",
19
- );
17
+ const actual = await vi.importActual<
18
+ typeof import("@elizaos/app-core/hooks")
19
+ >("@elizaos/app-core/hooks");
20
20
  return {
21
21
  ...actual,
22
22
  useBugReport: () => mockUseBugReport(),
@@ -73,9 +73,9 @@ vi.mock("@elizaos/app-core/platform", () => ({
73
73
  }));
74
74
 
75
75
  vi.mock("@elizaos/app-core/hooks", async () => {
76
- const actual = await vi.importActual<typeof import("@elizaos/app-core/hooks")>(
77
- "@elizaos/app-core/hooks",
78
- );
76
+ const actual = await vi.importActual<
77
+ typeof import("@elizaos/app-core/hooks")
78
+ >("@elizaos/app-core/hooks");
79
79
  return {
80
80
  ...actual,
81
81
  useVoiceChat: (...args: unknown[]) => mockUseVoiceChat(...args),
@@ -74,6 +74,7 @@ vi.mock("@elizaos/app-core/api", () => ({
74
74
  SkillScanReportSummary: {},
75
75
  }));
76
76
 
77
+ import type { AppState } from "@elizaos/app-core/state";
77
78
  import { AppProvider, useApp } from "@elizaos/app-core/state";
78
79
 
79
80
  function createDeferred<T>() {
@@ -99,8 +100,8 @@ function Probe(props: { onReady: (api: ProbeApi) => void }) {
99
100
 
100
101
  useEffect(() => {
101
102
  onReady({
102
- // biome-ignore lint/suspicious/noExplicitAny: test probe
103
- setState: (key, value) => app.setState(key as any, value),
103
+ setState: (key, value) =>
104
+ app.setState(key as keyof AppState, value as AppState[keyof AppState]),
104
105
  handleOnboardingNext: app.handleOnboardingNext,
105
106
  handleOnboardingBack: app.handleOnboardingBack,
106
107
  handleCloudLogin: app.handleCloudLogin,
@@ -29,9 +29,9 @@ vi.mock("@elizaos/app-core/platform", () => ({
29
29
  }));
30
30
 
31
31
  vi.mock("@elizaos/app-core/hooks", async () => {
32
- const actual = await vi.importActual<typeof import("@elizaos/app-core/hooks")>(
33
- "@elizaos/app-core/hooks",
34
- );
32
+ const actual = await vi.importActual<
33
+ typeof import("@elizaos/app-core/hooks")
34
+ >("@elizaos/app-core/hooks");
35
35
  return {
36
36
  ...actual,
37
37
  useVoiceChat: () => mockUseVoiceChat(),
@@ -7,9 +7,9 @@ const { mockUseApp } = vi.hoisted(() => ({
7
7
  }));
8
8
 
9
9
  vi.mock("@elizaos/app-core/state", async () => {
10
- const actual = await vi.importActual<typeof import("@elizaos/app-core/state")>(
11
- "@elizaos/app-core/state",
12
- );
10
+ const actual = await vi.importActual<
11
+ typeof import("@elizaos/app-core/state")
12
+ >("@elizaos/app-core/state");
13
13
  return {
14
14
  ...actual,
15
15
  useApp: () => mockUseApp(),
@@ -35,9 +35,9 @@ const { companionOverlayTabs, mockUseApp, noop } = vi.hoisted(() => ({
35
35
  }));
36
36
 
37
37
  vi.mock("@elizaos/app-core/state", async () => {
38
- const actual = await vi.importActual<typeof import("@elizaos/app-core/state")>(
39
- "@elizaos/app-core/state",
40
- );
38
+ const actual = await vi.importActual<
39
+ typeof import("@elizaos/app-core/state")
40
+ >("@elizaos/app-core/state");
41
41
  return {
42
42
  ...actual,
43
43
  useApp: () => mockUseApp(),
@@ -269,9 +269,9 @@ vi.mock("../../../packages/app-core/src/components/LifoSandboxView", () => ({
269
269
  React.createElement("section", null, "LifoSandboxView Ready"),
270
270
  }));
271
271
  vi.mock("@elizaos/app-core/hooks", async () => {
272
- const actual = await vi.importActual<typeof import("@elizaos/app-core/hooks")>(
273
- "@elizaos/app-core/hooks",
274
- );
272
+ const actual = await vi.importActual<
273
+ typeof import("@elizaos/app-core/hooks")
274
+ >("@elizaos/app-core/hooks");
275
275
  return {
276
276
  ...actual,
277
277
  useContextMenu: () => ({
@@ -396,7 +396,9 @@ describe("pages navigation smoke (e2e)", () => {
396
396
  conversations: [],
397
397
  elizaCloudCredits: null,
398
398
  uiShellMode: "companion",
399
- setUiShellMode: vi.fn(),
399
+ setUiShellMode: vi.fn((mode: "native" | "companion") => {
400
+ state.uiShellMode = mode;
401
+ }),
400
402
  uiLanguage: "en",
401
403
  agentStatus: { state: "running", agentName: "Milady" },
402
404
  loadDropStatus: vi.fn(),
@@ -587,7 +589,9 @@ describe("pages navigation smoke (e2e)", () => {
587
589
  conversations: [],
588
590
  elizaCloudCredits: null,
589
591
  uiShellMode: "companion",
590
- setUiShellMode: vi.fn(),
592
+ setUiShellMode: vi.fn((mode: "native" | "companion") => {
593
+ state.uiShellMode = mode;
594
+ }),
591
595
  uiLanguage: "en",
592
596
  agentStatus: { state: "running", agentName: "Milady" },
593
597
  loadDropStatus: vi.fn(),
@@ -600,6 +604,7 @@ describe("pages navigation smoke (e2e)", () => {
600
604
  setActionNotice: vi.fn(),
601
605
  setTab: (tab: Tab) => {
602
606
  state.tab = tab;
607
+ state.uiShellMode = shellModeForTab(tab);
603
608
  },
604
609
  };
605
610
  Object.assign(state, entry.patch);
@@ -2,7 +2,6 @@
2
2
  /**
3
3
  * Tests for plugin-bridge — capabilities detection and feature flags on web platform.
4
4
  */
5
- import { describe, expect, it, vi } from "vitest";
6
5
 
7
6
  import {
8
7
  getPluginCapabilities,
@@ -15,6 +14,7 @@ import {
15
14
  isWeb,
16
15
  platform,
17
16
  } from "@elizaos/app-core/bridge";
17
+ import { describe, expect, it, vi } from "vitest";
18
18
 
19
19
  describe("plugin-bridge", () => {
20
20
  // -- Platform --
@@ -85,8 +85,7 @@ function defaultProps() {
85
85
  }
86
86
 
87
87
  function getSelectValue(tree: ReactTestRenderer): string | undefined {
88
- // biome-ignore lint/suspicious/noExplicitAny: test introspection
89
- const root = (tree as any).root;
88
+ const root = tree.root;
90
89
  const selects = root.findAll(
91
90
  (node: ReactTestInstance) =>
92
91
  node.type === "select" && node.props.value !== undefined,
@@ -95,8 +94,7 @@ function getSelectValue(tree: ReactTestRenderer): string | undefined {
95
94
  }
96
95
 
97
96
  function getSelect(tree: ReactTestRenderer): ReactTestInstance {
98
- // biome-ignore lint/suspicious/noExplicitAny: test introspection
99
- const root = (tree as any).root;
97
+ const root = tree.root;
100
98
  return root.find(
101
99
  (node: ReactTestInstance) =>
102
100
  node.type === "select" && node.props.value !== undefined,
@@ -14,9 +14,9 @@ interface RestartBannerContextStub {
14
14
  const mockUseApp = vi.fn<() => RestartBannerContextStub>();
15
15
 
16
16
  vi.mock("@elizaos/app-core/state", async () => {
17
- const actual = await vi.importActual<typeof import("@elizaos/app-core/state")>(
18
- "@elizaos/app-core/state",
19
- );
17
+ const actual = await vi.importActual<
18
+ typeof import("@elizaos/app-core/state")
19
+ >("@elizaos/app-core/state");
20
20
  return {
21
21
  ...actual,
22
22
  useApp: () => mockUseApp(),
@@ -49,9 +49,9 @@ vi.mock("@elizaos/app-core/platform", async () => {
49
49
  /* ── Mock every leaf component ────────────────────────────────────── */
50
50
 
51
51
  vi.mock("@elizaos/app-core/state", async () => {
52
- const actual = await vi.importActual<typeof import("@elizaos/app-core/state")>(
53
- "@elizaos/app-core/state",
54
- );
52
+ const actual = await vi.importActual<
53
+ typeof import("@elizaos/app-core/state")
54
+ >("@elizaos/app-core/state");
55
55
  return {
56
56
  ...actual,
57
57
  useApp: () => mockUseApp(),
@@ -308,9 +308,9 @@ vi.mock("../../../packages/app-core/src/components/LifoSandboxView", () => ({
308
308
  }));
309
309
 
310
310
  vi.mock("@elizaos/app-core/hooks", async () => {
311
- const actual = await vi.importActual<typeof import("@elizaos/app-core/hooks")>(
312
- "@elizaos/app-core/hooks",
313
- );
311
+ const actual = await vi.importActual<
312
+ typeof import("@elizaos/app-core/hooks")
313
+ >("@elizaos/app-core/hooks");
314
314
  return {
315
315
  ...actual,
316
316
  useContextMenu: () => ({
@@ -1,5 +1,5 @@
1
- import { describe, expect, it } from "vitest";
2
1
  import type { Tab } from "@elizaos/app-core/navigation";
2
+ import { describe, expect, it } from "vitest";
3
3
  import {
4
4
  deriveUiShellModeForTab,
5
5
  getTabForShellView,
@@ -11,9 +11,9 @@ const { mockUseApp } = vi.hoisted(() => ({
11
11
  }));
12
12
 
13
13
  vi.mock("@elizaos/app-core/state", async () => {
14
- const actual = await vi.importActual<typeof import("@elizaos/app-core/state")>(
15
- "@elizaos/app-core/state",
16
- );
14
+ const actual = await vi.importActual<
15
+ typeof import("@elizaos/app-core/state")
16
+ >("@elizaos/app-core/state");
17
17
  return {
18
18
  ...actual,
19
19
  useApp: () => mockUseApp(),
@@ -475,8 +475,9 @@ describe("TriggersView UI E2E", () => {
475
475
  runtimeHarness = createTriggerRuntimeHarness();
476
476
  server = await startApiServerFn({
477
477
  port: 0,
478
- // biome-ignore lint/suspicious/noExplicitAny: test mock
479
- runtime: runtimeHarness.runtime as any,
478
+ runtime: runtimeHarness.runtime as Parameters<
479
+ typeof startApiServerFn
480
+ >[0]["runtime"],
480
481
  });
481
482
  });
482
483
 
@@ -1,8 +1,9 @@
1
1
  // @vitest-environment jsdom
2
+
3
+ import type { WalletConfigUpdateRequest } from "@elizaos/autonomous/contracts/wallet";
2
4
  import React, { useEffect } from "react";
3
5
  import TestRenderer, { act } from "react-test-renderer";
4
6
  import { beforeEach, describe, expect, it, vi } from "vitest";
5
- import type { WalletConfigUpdateRequest } from "@elizaos/autonomous/contracts/wallet";
6
7
 
7
8
  const { mockClient } = vi.hoisted(() => ({
8
9
  mockClient: {
@@ -0,0 +1,64 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { stripAssistantStageDirections } from "../../src/utils/assistant-text";
3
+
4
+ describe("stripAssistantStageDirections", () => {
5
+ it("strips asterisk-wrapped stage directions", () => {
6
+ const result = stripAssistantStageDirections("Hello *smiles warmly* there");
7
+ expect(result).not.toContain("smiles warmly");
8
+ expect(result).toContain("Hello");
9
+ expect(result).toContain("there");
10
+ });
11
+
12
+ it("strips underscore-wrapped stage directions", () => {
13
+ const result = stripAssistantStageDirections("Hello _waves happily_ there");
14
+ expect(result).not.toContain("waves happily");
15
+ });
16
+
17
+ it("preserves asterisk content that is NOT a stage direction", () => {
18
+ const result = stripAssistantStageDirections("Use *bold text* for emphasis");
19
+ expect(result).toContain("bold text");
20
+ });
21
+
22
+ it("preserves underscore content that is NOT a stage direction", () => {
23
+ const result = stripAssistantStageDirections("Use _italic text_ for emphasis");
24
+ expect(result).toContain("italic text");
25
+ });
26
+
27
+ it("handles empty input", () => {
28
+ expect(stripAssistantStageDirections("")).toBe("");
29
+ });
30
+
31
+ it("handles text with no stage directions", () => {
32
+ expect(stripAssistantStageDirections("Just plain text")).toBe(
33
+ "Just plain text",
34
+ );
35
+ });
36
+
37
+ it("handles multiple stage directions in one message", () => {
38
+ const result = stripAssistantStageDirections(
39
+ "*nods* I agree. *smiles* That sounds right.",
40
+ );
41
+ expect(result).not.toContain("nods");
42
+ expect(result).not.toContain("smiles");
43
+ expect(result).toContain("I agree.");
44
+ expect(result).toContain("That sounds right.");
45
+ });
46
+
47
+ it("handles stage direction at start of text", () => {
48
+ const result = stripAssistantStageDirections("*laughs* That's funny!");
49
+ expect(result).not.toContain("laughs");
50
+ expect(result).toContain("That's funny!");
51
+ });
52
+
53
+ it("handles stage direction at end of text", () => {
54
+ const result = stripAssistantStageDirections("Goodbye! *waves*");
55
+ expect(result).not.toContain("waves");
56
+ expect(result).toContain("Goodbye!");
57
+ });
58
+
59
+ it("does not strip across newlines (asterisk pattern is non-greedy single line)", () => {
60
+ const input = "*smiles\nacross lines*";
61
+ const result = stripAssistantStageDirections(input);
62
+ expect(result).toContain("smiles");
63
+ });
64
+ });
@@ -0,0 +1,89 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import {
3
+ mergeStreamingText,
4
+ computeStreamingDelta,
5
+ } from "../../src/utils/streaming-text";
6
+
7
+ describe("mergeStreamingText", () => {
8
+ it("returns incoming when it starts with existing (cumulative snapshot)", () => {
9
+ expect(mergeStreamingText("Hello", "Hello world")).toBe("Hello world");
10
+ });
11
+
12
+ it("returns incoming when existing is empty", () => {
13
+ expect(mergeStreamingText("", "Hello")).toBe("Hello");
14
+ });
15
+
16
+ it("returns existing when incoming is empty", () => {
17
+ expect(mergeStreamingText("Hello", "")).toBe("Hello");
18
+ });
19
+
20
+ it("returns existing when incoming equals existing", () => {
21
+ expect(mergeStreamingText("Hello", "Hello")).toBe("Hello");
22
+ });
23
+
24
+ it("keeps existing when incoming is a prefix (regressive snapshot)", () => {
25
+ expect(mergeStreamingText("Hello world", "Hello")).toBe("Hello world");
26
+ });
27
+
28
+ it("appends new portion when incoming overlaps existing suffix", () => {
29
+ expect(mergeStreamingText("Hello wor", "world")).toBe("Hello world");
30
+ });
31
+
32
+ it("appends single character even when it matches last char (repeated chars)", () => {
33
+ expect(mergeStreamingText("Hel", "l")).toBe("Hell");
34
+ });
35
+
36
+ it("builds up character by character correctly", () => {
37
+ let text = "";
38
+ for (const char of "Hello") {
39
+ text = mergeStreamingText(text, char);
40
+ }
41
+ expect(text).toBe("Hello");
42
+ });
43
+
44
+ it("drops suffix-only resend larger than 1 character", () => {
45
+ expect(mergeStreamingText("Hello world", "world")).toBe("Hello world");
46
+ });
47
+
48
+ it("replaces when incoming shares prefix >=8 chars but differs", () => {
49
+ expect(
50
+ mergeStreamingText(
51
+ "The quick brown fox jumps",
52
+ "The quick brown fox leaps",
53
+ ),
54
+ ).toBe("The quick brown fox leaps");
55
+ });
56
+
57
+ it("does not false-positive on short messages with natural overlap", () => {
58
+ expect(mergeStreamingText("Hi", "Hi there")).toBe("Hi there");
59
+ });
60
+
61
+ it("returns incoming when it contains existing as substring", () => {
62
+ expect(mergeStreamingText("Hello", "prefix Hello suffix")).toBe(
63
+ "prefix Hello suffix",
64
+ );
65
+ });
66
+
67
+ it("concatenates when no overlap detected and not a snapshot", () => {
68
+ expect(mergeStreamingText("abc", "xyz")).toBe("abcxyz");
69
+ });
70
+ });
71
+
72
+ describe("computeStreamingDelta", () => {
73
+ it("returns empty string when merge result equals existing", () => {
74
+ expect(computeStreamingDelta("Hello world", "Hello")).toBe("");
75
+ });
76
+
77
+ it("returns only the new portion for cumulative snapshots", () => {
78
+ expect(computeStreamingDelta("Hello", "Hello world")).toBe(" world");
79
+ });
80
+
81
+ it("returns incoming for snapshot replacements", () => {
82
+ expect(
83
+ computeStreamingDelta(
84
+ "The quick brown fox jumps",
85
+ "The quick brown fox leaps",
86
+ ),
87
+ ).toBe("The quick brown fox leaps");
88
+ });
89
+ });