Skip to content

Commit 136c11c

Browse files
committed
Merge origin/develop into develop
2 parents 94ae229 + 1637819 commit 136c11c

File tree

7 files changed

+187
-16
lines changed

7 files changed

+187
-16
lines changed

packages/app-core/src/App.tsx

Lines changed: 39 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import {
4242
StreamView,
4343
SystemWarningBanner,
4444
} from "./components";
45+
import { CompanionHeader } from "./components/companion/CompanionHeader";
4546
import { DeferredSetupChecklist } from "./components/FlaminaGuide";
4647
import {
4748
BugReportProvider,
@@ -241,12 +242,23 @@ export function App() {
241242
retryStartup,
242243
tab,
243244
setTab,
245+
setState,
244246
actionNotice,
245247
uiShellMode,
248+
switchShellView,
249+
uiLanguage,
250+
setUiLanguage,
251+
uiTheme,
252+
setUiTheme,
253+
chatAgentVoiceMuted,
254+
handleSaveCharacter,
255+
characterSaving,
256+
characterSaveSuccess,
246257
agentStatus,
247258
unreadConversations,
248259
activeGameViewerUrl,
249260
gameOverlayEnabled,
261+
t,
250262
} = useApp();
251263

252264
const isPopout = useIsPopout();
@@ -534,19 +546,34 @@ export function App() {
534546
/>
535547
</div>
536548
</div>
549+
) : characterSceneVisible ? (
550+
<div className="relative flex flex-col flex-1 min-h-0 w-full font-body text-txt bg-transparent">
551+
<CompanionHeader
552+
activeShellView="character"
553+
onShellViewChange={(view) => switchShellView(view)}
554+
uiLanguage={uiLanguage}
555+
setUiLanguage={setUiLanguage}
556+
uiTheme={uiTheme}
557+
setUiTheme={setUiTheme}
558+
t={t}
559+
showCompanionControls
560+
chatAgentVoiceMuted={chatAgentVoiceMuted}
561+
onToggleVoiceMute={() =>
562+
setState("chatAgentVoiceMuted", !chatAgentVoiceMuted)
563+
}
564+
onSave={handleSaveCharacter}
565+
isSaving={characterSaving}
566+
saveSuccess={Boolean(characterSaveSuccess)}
567+
/>
568+
<main className="flex flex-1 min-h-0 min-w-0 overflow-hidden px-3 xl:px-5 pb-4 pt-2 xl:pb-6">
569+
<ViewRouter characterSceneVisible />
570+
</main>
571+
</div>
537572
) : (
538-
<div
539-
className={`flex flex-col flex-1 min-h-0 w-full font-body text-txt ${
540-
characterSceneVisible ? "bg-transparent" : "bg-bg"
541-
}`}
542-
>
543-
<Header transparent={characterSceneVisible} />
544-
<main
545-
className={`flex flex-1 min-h-0 min-w-0 overflow-hidden px-3 xl:px-5 ${
546-
characterSceneVisible ? "pb-4 pt-2 xl:pb-6" : "py-4 xl:py-6"
547-
}`}
548-
>
549-
<ViewRouter characterSceneVisible={characterSceneVisible} />
573+
<div className="flex flex-col flex-1 min-h-0 w-full font-body text-txt bg-bg">
574+
<Header />
575+
<main className="flex flex-1 min-h-0 min-w-0 overflow-hidden px-3 xl:px-5 py-4 xl:py-6">
576+
<ViewRouter />
550577
</main>
551578
</div>
552579
);

packages/app-core/src/components/Header.tsx

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ export function Header({
7373
setUiTheme,
7474
chatAgentVoiceMuted,
7575
handleNewConversation,
76+
handleSaveCharacter,
77+
characterSaving,
78+
characterSaveSuccess,
7679
conversationMessages,
7780
chatLastUsage,
7881
t,
@@ -271,7 +274,20 @@ export function Header({
271274
onToggleVoiceMute={() =>
272275
setState("chatAgentVoiceMuted", !chatAgentVoiceMuted)
273276
}
274-
onNewChat={() => void handleNewConversation()}
277+
onNewChat={
278+
activeShellView === "character"
279+
? undefined
280+
: () => void handleNewConversation()
281+
}
282+
onSave={
283+
activeShellView === "character" ? handleSaveCharacter : undefined
284+
}
285+
isSaving={activeShellView === "character" ? characterSaving : false}
286+
saveSuccess={
287+
activeShellView === "character"
288+
? Boolean(characterSaveSuccess)
289+
: false
290+
}
275291
trailingExtras={
276292
showNavigationMenu ? (
277293
<Button

packages/app-core/src/components/companion/CompanionHeader.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ export interface CompanionHeaderProps {
1717
chatAgentVoiceMuted?: boolean;
1818
onToggleVoiceMute?: () => void;
1919
onNewChat?: () => void;
20+
onSave?: () => void;
21+
isSaving?: boolean;
22+
saveSuccess?: boolean;
2023
/** Shown in the shell header right cluster (e.g. inference / cloud alert). */
2124
rightExtras?: ReactNode;
2225
rightTrailingExtras?: ReactNode;
@@ -38,6 +41,9 @@ export const CompanionHeader = memo(function CompanionHeader(
3841
chatAgentVoiceMuted,
3942
onToggleVoiceMute,
4043
onNewChat,
44+
onSave,
45+
isSaving,
46+
saveSuccess,
4147
rightExtras,
4248
rightTrailingExtras,
4349
} = props;
@@ -81,6 +87,9 @@ export const CompanionHeader = memo(function CompanionHeader(
8187
chatAgentVoiceMuted={chatAgentVoiceMuted}
8288
onToggleVoiceMute={onToggleVoiceMute}
8389
onNewChat={onNewChat}
90+
onSave={onSave}
91+
isSaving={isSaving}
92+
saveSuccess={saveSuccess}
8493
rightExtras={rightExtras}
8594
rightTrailingExtras={rightTrailingExtras}
8695
>

packages/app-core/src/components/companion/ShellHeaderControls.test.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,12 @@ vi.mock("@miladyai/ui", () => ({
2828
}));
2929

3030
vi.mock("lucide-react", () => ({
31+
Check: () => React.createElement("span", null, "check"),
32+
Loader2: () => React.createElement("span", null, "loader"),
3133
MessageCirclePlus: () => React.createElement("span", null, "new"),
3234
Monitor: () => React.createElement("span", null, "desktop"),
3335
PencilLine: () => React.createElement("span", null, "character"),
36+
Save: () => React.createElement("span", null, "save"),
3437
Smartphone: () => React.createElement("span", null, "phone"),
3538
UserRound: () => React.createElement("span", null, "companion"),
3639
Volume2: () => React.createElement("span", null, "voice"),
@@ -194,4 +197,38 @@ describe("ShellHeaderControls", () => {
194197
null,
195198
]);
196199
});
200+
201+
it("renders Save button instead of New Chat when onSave is provided", async () => {
202+
mockUseMediaQuery.mockReturnValue(false);
203+
const onSave = vi.fn();
204+
let tree: ReactTestRenderer | undefined;
205+
await act(async () => {
206+
tree = create(
207+
<ShellHeaderControls
208+
activeShellView="character"
209+
onShellViewChange={() => {}}
210+
uiLanguage="en"
211+
setUiLanguage={() => {}}
212+
uiTheme="dark"
213+
setUiTheme={() => {}}
214+
t={(k: string) => k}
215+
showCompanionControls
216+
onSave={onSave}
217+
/>,
218+
);
219+
});
220+
const buttons = tree!.root.findAll(
221+
(node) =>
222+
node.type === "button" &&
223+
node.props["aria-label"] === "charactereditor.Save",
224+
);
225+
expect(buttons.length).toBeGreaterThanOrEqual(1);
226+
// New Chat should NOT be present
227+
const newChatButtons = tree!.root.findAll(
228+
(node) =>
229+
node.type === "button" &&
230+
node.props["aria-label"] === "companion.newChat",
231+
);
232+
expect(newChatButtons.length).toBe(0);
233+
});
197234
});

packages/app-core/src/components/companion/ShellHeaderControls.tsx

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@ import type { ShellView, UiTheme } from "@miladyai/app-core/state";
55
import { Button } from "@miladyai/ui";
66
import {
77
type LucideIcon,
8+
Check,
9+
Loader2,
810
MessageCirclePlus,
911
Monitor,
1012
PencilLine,
13+
Save,
1114
Smartphone,
1215
UserRound,
1316
Volume2,
@@ -58,6 +61,9 @@ interface ShellHeaderControlsProps {
5861
chatAgentVoiceMuted?: boolean;
5962
onToggleVoiceMute?: () => void;
6063
onNewChat?: () => void;
64+
onSave?: () => void;
65+
isSaving?: boolean;
66+
saveSuccess?: boolean;
6167
}
6268

6369
export function ShellHeaderControls({
@@ -84,6 +90,9 @@ export function ShellHeaderControls({
8490
chatAgentVoiceMuted = false,
8591
onToggleVoiceMute,
8692
onNewChat,
93+
onSave,
94+
isSaving = false,
95+
saveSuccess = false,
8796
}: ShellHeaderControlsProps) {
8897
const isMobileViewport = useMediaQuery(SHELL_MODE_MOBILE_MEDIA_QUERY);
8998
const shouldSplitCompanionMobileActions =
@@ -170,6 +179,47 @@ export function ShellHeaderControls({
170179
</Button>
171180
);
172181

182+
const renderSaveButton = (compact: boolean) => (
183+
<Button
184+
size="icon"
185+
variant="outline"
186+
aria-label={t("charactereditor.Save")}
187+
title={t("charactereditor.Save")}
188+
className={
189+
compact
190+
? compactCompanionActionClassName
191+
: expandedCompanionActionClassName
192+
}
193+
onClick={onSave}
194+
disabled={isSaving}
195+
onPointerDown={(event) => event.stopPropagation()}
196+
style={HEADER_BUTTON_STYLE}
197+
data-no-camera-drag="true"
198+
>
199+
{isSaving ? (
200+
<Loader2 className="pointer-events-none h-4 w-4 shrink-0 animate-spin" />
201+
) : saveSuccess ? (
202+
<Check className="pointer-events-none h-4 w-4 shrink-0 text-green-400" />
203+
) : (
204+
<Save className="pointer-events-none h-4 w-4 shrink-0" />
205+
)}
206+
<span className="pointer-events-none hidden sm:inline">
207+
{isSaving
208+
? t("charactereditor.Saving")
209+
: saveSuccess
210+
? t("charactereditor.Saved")
211+
: t("charactereditor.Save")}
212+
</span>
213+
</Button>
214+
);
215+
216+
/** Render the appropriate action button — Save for character, New Chat for companion */
217+
const renderActionButton = (compact: boolean) => {
218+
if (onSave) return renderSaveButton(compact);
219+
if (onNewChat) return renderNewChatButton(compact);
220+
return null;
221+
};
222+
173223
return (
174224
<div
175225
className={`min-w-0 w-full overflow-visible gap-2 ${
@@ -250,7 +300,7 @@ export function ShellHeaderControls({
250300
>
251301
<div className="inline-flex items-center gap-2">
252302
{renderVoiceButton(false)}
253-
{renderNewChatButton(false)}
303+
{renderActionButton(false)}
254304
</div>
255305
</div>
256306
)
@@ -276,7 +326,7 @@ export function ShellHeaderControls({
276326
data-testid="companion-header-desktop-new-chat"
277327
data-no-camera-drag="true"
278328
>
279-
{renderNewChatButton(false)}
329+
{renderActionButton(false)}
280330
</div>
281331
) : null}
282332
{rightTrailingExtras}
@@ -324,7 +374,7 @@ export function ShellHeaderControls({
324374
className="flex items-center justify-end"
325375
data-testid="companion-header-mobile-new-chat"
326376
>
327-
{renderNewChatButton(true)}
377+
{renderActionButton(true)}
328378
</div>
329379
</div>
330380
) : null}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { existsSync, readFileSync } from "node:fs";
2+
import path from "node:path";
3+
import { describe, expect, it } from "vitest";
4+
5+
/**
6+
* Contract test: character editor uses CompanionHeader (glassmorphic bar),
7+
* not the native Header.
8+
*/
9+
describe("character editor header", () => {
10+
it("App.tsx renders CompanionHeader when characterSceneVisible", () => {
11+
const appPath = path.resolve(import.meta.dirname, "..", "..", "App.tsx");
12+
expect(existsSync(appPath)).toBe(true);
13+
const source = readFileSync(appPath, "utf-8");
14+
15+
// characterSceneVisible path should use CompanionHeader, not Header
16+
const charBlock = source.indexOf("characterSceneVisible ?");
17+
expect(charBlock).toBeGreaterThan(-1);
18+
const after = source.slice(charBlock, charBlock + 800);
19+
expect(after).toContain("CompanionHeader");
20+
expect(after).toContain('activeShellView="character"');
21+
expect(after).toContain("onSave");
22+
});
23+
24+
it("CompanionHeader accepts onSave/isSaving/saveSuccess props", () => {
25+
const headerPath = path.resolve(import.meta.dirname, "CompanionHeader.tsx");
26+
const source = readFileSync(headerPath, "utf-8");
27+
expect(source).toContain("onSave?:");
28+
expect(source).toContain("isSaving?:");
29+
expect(source).toContain("saveSuccess?:");
30+
});
31+
});

packages/app-core/src/i18n/locales/en.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1047,6 +1047,7 @@
10471047
"charactereditor.AddPost": "Add Post",
10481048
"charactereditor.Saving": "saving...",
10491049
"charactereditor.Save": "Save",
1050+
"charactereditor.Saved": "Saved",
10501051
"charactereditor.SelectBtn": "Select",
10511052
"charactereditor.CustomizeBtn": "Customize",
10521053
"configpageview.ConnectedToElizaCloud": "Connected to Eliza Cloud",

0 commit comments

Comments
 (0)