Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
ab6f3f9
fix(termux): keep the server up when sharp's WebAssembly runtime is m…
SpicyMarinara Oct 6, 2026
2ec45b4
docs: Termux image library repair and crash note (#7173)
SpicyMarinara Oct 6, 2026
d1f3563
docs: existing thumbnails still show while Sharp cannot load (#7173)
SpicyMarinara Oct 6, 2026
a3b74c4
docs: only the built-in background removal needs Sharp (#7173)
SpicyMarinara Oct 6, 2026
d6b2feb
chore(android): bump versionCode to 49 for the refreshed 2.5.0 APK
SpicyMarinara Oct 6, 2026
25f464f
fix(chat): keep chat controls off other screens and let the Agents bu…
SpicyMarinara Oct 6, 2026
11e6eb4
test(e2e): make the covered-chat regression's return step deterministic
SpicyMarinara Oct 6, 2026
27b3d5f
test(e2e): cover a pinned chat window behind covering screens
SpicyMarinara Oct 6, 2026
4828ebb
fix(chat): move keyboard focus into the Tracker Panel when it covers …
SpicyMarinara Oct 6, 2026
4fe3965
docs: the Tracker Panel button opens and closes the panel (#7176)
SpicyMarinara Oct 6, 2026
888e011
test(e2e): make the 5,001-row trash batching proof reliable on slow W…
SpicyMarinara Oct 7, 2026
1fb1c7b
test(e2e): keep the trash toast visibility check in the mount record
SpicyMarinara Oct 7, 2026
8845cd4
fix(termux): remove the dead .npmrc architecture block
SpicyMarinara Oct 6, 2026
7125d02
test(termux): pin the default pnpm target in every config pnpm reads
SpicyMarinara Oct 6, 2026
8b0bf98
test(termux): parse lockfile entries by key and reject negated archit…
SpicyMarinara Oct 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ android {
applicationId "com.marinara.engine"
minSdk 24
targetSdk 34
versionCode 48
versionCode 49
versionName "2.5.0"
buildConfigField "String", "MARINARA_SERVER_URL", "\"http://127.0.0.1:${marinaraPort}\""
buildConfigField "String", "MARINARA_RELEASE_TAG", "\"v2.5.0\""
Expand Down
19 changes: 17 additions & 2 deletions docs/TROUBLESHOOTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,11 +31,26 @@ If Vite stops with `Reached heap limit` or `JavaScript heap out of memory`, the

Close other apps before retrying. Phones with less than about 3 GB of RAM can still run out of memory or be stopped by Android; keep the complete launcher output when reporting that case. Do not delete your chats or profile to repair a build failure.

### Termux: missing multiplayer guest asset or incompatible Sharp
### Termux: missing multiplayer guest asset

If startup still reports a missing `packages/client/dist/multiplayer/guest.js` after rebuilding, update Engine and rerun `./start-termux.sh`. The launcher now runs the complete low-memory client build, including the guest assets checked at startup. You do not need to enable multiplayer to repair this build error.

If image processing reports that Sharp cannot load on Android, update Engine and let the launcher reinstall dependencies. The matching `@img/sharp-wasm32` fallback is included as a regular dependency so frozen installs and updates retain it. Avoid replacing it with an unrelated Sharp version. Keep the complete error output if the problem persists.
### Termux: server stops when you open Engine, or Sharp cannot load

Sharp is the image library Engine uses for thumbnails and sprites. On Android it runs through a WebAssembly fallback. Updating in place from 2.4.6 could leave part of that fallback uninstalled, and the server then stopped the first time a browser opened Engine.

Update Engine and let the launcher reinstall dependencies. The update installs the missing part, and Engine keeps it through later updates. If the server keeps stopping when you open Engine before you can update, repair the install by hand in Termux:

```bash
cd ~/Marinara-Engine
rm -f node_modules/.modules.yaml
SHARP_IGNORE_GLOBAL_LIBVIPS=1 pnpm --config.trustPolicy=off --config.confirmModulesPurge=false install --frozen-lockfile --prefer-offline
./start-termux.sh
```

This clears pnpm's outdated install record and reinstalls the dependencies. Your chats and settings are not touched.

If Sharp still cannot load, Engine now keeps running with image processing off: thumbnails it has already made still show, new ones show the full-size image instead, and sprite generation and the built-in background removal are unavailable. Do not replace Sharp with an unrelated version. Keep the complete error output when you report the problem.

### Blank page or JavaScript served as HTML after an update

Expand Down
2 changes: 1 addition & 1 deletion docs/chats/chat-settings.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ The title bar has a few other useful buttons:
- **Help** (the **?** beside the title) shows labels explaining the chat's controls. On a phone, it closes Chat Settings first so you can see the chat. Tap the highlighted **Chat Settings** button for a guide to its icons: Reset View, favorite layout, the Roleplay Tracker Panel, lock/unlock, close and moving a section out. Scroll inside the guide to read every item. You can hide the Help button in **Settings → General → App Behavior → Hide chat Help button**.
- **Reset View** (the circular arrow) asks for confirmation, then restores this chat's starting layout. It puts popped-out sections back and restores the default window sizes, positions, pins and locks.
- **Favorite layout** (the star beside Reset View) saves this arrangement for new chats in the current mode. See [Choosing a layout for new chats](#choosing-a-layout-for-new-chats).
- **Tracker Panel** (the die) turns the panel on or off in a Roleplay chat that uses agents or Advanced Memory. On a computer, the panel opens beside the chat. On a phone, a movable button opens it. This setting also changes the Tracker Panel preference in **Settings → Appearance**.
- **Tracker Panel** (the die) turns the panel on or off in a Roleplay chat that uses agents or Advanced Memory. On a computer, the panel opens beside the chat. On a phone or tablet, a movable button opens and closes it. This setting also changes the Tracker Panel preference in **Settings → Appearance**.

When the Tracker Panel is not shown, desktop trackers use the [Trackers window](../roleplay/hud-and-trackers.md#the-trackers-window). When there is not enough room beside your messages, it starts as a small **Trackers** button. Closing the window returns it to that button. Use the die in the Chat Settings title bar to show the Tracker Panel again.

Expand Down
2 changes: 1 addition & 1 deletion docs/roleplay/hud-and-trackers.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ The **Tracker Panel** is a larger side panel that shows the same tracker data as

To turn it on in a Roleplay chat, open **Chat Settings** and click the **Tracker Panel** button (the die) in its title bar, next to pin and lock. It stays highlighted while the panel is on, and the panel shows beside the chat. Click it again to turn the panel off and hide it. On a computer, the trackers then show in the Trackers window.

On a phone, turning it on puts a Tracker Panel button on the chat that you can drag anywhere. Tap it to open the panel; closing the panel goes back to the button. With the panel off, use the separate **World State** and **Player & Tracker** buttons.
On a phone or tablet, turning it on puts a Tracker Panel button on the chat that you can drag anywhere. Tap it to open the panel, and tap it again to close it. While the panel or another screen covers the chat, the chat's movable buttons are hidden, and they come back in the same places when you return to the chat. With the panel off, use the separate **World State** and **Player & Tracker** buttons.

The controls in the panel header also let you customize tracker structure:

Expand Down
229 changes: 229 additions & 0 deletions e2e/chat-controls-covered.e2e.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
// Issue #7176: between phone and desktop widths (an 856px foldable, say) panels and editors open
// over a chat that stays mounted. The chat's buttons and windows must not show through them, and
// the Trackers button must close the Tracker Panel it opened.
import { expect, test, type APIRequestContext, type Locator, type Page, type TestInfo } from "@playwright/test";
import { readFileSync } from "node:fs";
import { seedUIState } from "./ui-state-fixture.js";

const version = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
const ANDROID_APP_UA =
"Mozilla/5.0 (Linux; Android 14; Honor Magic V2) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36 MarinaraEngine/Android";
const AGENTS_PANEL = '[data-component="RightPanelMobile"], [data-component="RightPanelDesktopSlot"]';
test.use({ reducedMotion: "reduce" });

async function createChat(request: APIRequestContext) {
const response = await request.post("/api/chats", {
data: { name: "Covered chat controls", mode: "roleplay", characterIds: [] },
});
expect(response.ok()).toBeTruthy();
const chat = (await response.json()) as { id: string };
expect(
(await request.patch(`/api/chats/${chat.id}/metadata`, { data: { enableAgents: true, activeAgentIds: [] } })).ok(),
).toBeTruthy();
return chat;
}

async function openChat(page: Page, chatId: string) {
await page.route("**/api/app-settings/ui", (route) => route.fulfill({ json: { value: "" } }));
await seedUIState(page, {
hasCompletedOnboarding: true,
sidebarOpen: false,
rightPanelOpen: false,
chatHelpSeenModes: ["conversation", "roleplay", "game"],
chatSettingsMoveTipDismissed: true,
appAccentPulseMode: false,
trackerPanelEnabled: true,
trackerPanelOpen: true,
trackerPanelOpenByChatId: { [chatId]: true },
});
await page.addInitScript(
({ chatId, version }) => {
localStorage.setItem("marinara-active-chat-id", chatId);
localStorage.setItem("marinara:whats-new:seen-version", version);
},
{ chatId, version },
);
await page.goto("/");
await expect(page.locator('[data-chat-mode="roleplay"]')).toBeVisible({ timeout: 30_000 });
}

const center = async (locator: Locator) => {
const box = await locator.boundingBox();
expect(box).not.toBeNull();
return { x: box!.x + box!.width / 2, y: box!.y + box!.height / 2 };
};

/** Whether a press at this point reaches the given element (nothing drawn over it). */
const pressReaches = (page: Page, point: { x: number; y: number }, selector: string) =>
page.evaluate(({ x, y, selector }) => document.elementFromPoint(x, y)?.closest(selector) != null, {
...point,
selector,
});

async function snap(page: Page, info: TestInfo, name: string) {
await page.screenshot({ path: info.outputPath(`${name}.png`), animations: "disabled" });
}

/** The 856px flow: the Tracker Panel, then the Agents list and an agent's editor, all over the chat and a pinned window. */
async function checkCoveredChat(page: Page, info: TestInfo) {
const settingsButton = page.locator(".mari-window-bubble[data-chat-settings-button]");
const trackersButton = page.locator('.mari-window-bubble[data-tracker-panel-toggle="bubble"]');
const panel = page.locator('[data-component="TrackerDataSidebar"]:visible');
const chatButtons = page.locator(".mari-window-bubble:visible");

// The panel opens over the chat on load; the chat's buttons stay under it.
await expect(panel).toBeVisible();
await snap(page, info, "tracker-panel-open");
await expect(chatButtons).toHaveCount(0);

await panel.getByRole("button", { name: "Close tracker panel", exact: true }).click();
await expect(panel).toHaveCount(0);
await expect(settingsButton).toBeVisible();
await expect(trackersButton).toHaveAttribute("aria-expanded", "false");
const settingsPoint = await center(settingsButton);
const trackersPoint = await center(trackersButton);

// From the keyboard: the panel hides its button, so focus moves into the panel and back again on close.
await trackersButton.focus();
await page.keyboard.press("Enter");
await expect(panel).toBeVisible();
await expect(page.locator('[data-component="TrackerDataSidebarMobile"]')).toBeFocused();
await expect(chatButtons).toHaveCount(0);
await expect.poll(() => pressReaches(page, trackersPoint, ".mari-tracker-panel")).toBe(true);
await expect.poll(() => pressReaches(page, settingsPoint, ".mari-tracker-panel")).toBe(true);
await panel.getByRole("button", { name: "Close tracker panel", exact: true }).click();
await expect(trackersButton).toBeVisible();
await expect(trackersButton).toBeFocused();

// A pinned window stays open behind other screens, out of sight.
await settingsButton.click();
const settingsWindow = page.locator('.mari-window[data-window="chat-settings"]');
await expect(settingsWindow).toBeVisible();
await settingsWindow.locator('[data-window-control="pin"]').click();
await expect(settingsWindow).toHaveAttribute("data-pinned", "true");

// Another screen from the top bar, without going Home first.
await page.locator('[data-tour="panel-agents"]').click();
const newAgent = page.locator(AGENTS_PANEL).getByRole("button", { name: "New", exact: true });
await expect(newAgent).toBeVisible();
await snap(page, info, "agents-list");
await expect(chatButtons).toHaveCount(0);
await expect(settingsWindow).toBeHidden();
await expect.poll(() => pressReaches(page, trackersPoint, AGENTS_PANEL)).toBe(true);
await expect.poll(() => pressReaches(page, settingsPoint, AGENTS_PANEL)).toBe(true);

await newAgent.click();
const editor = page.locator('[data-component="MobileDetailSheet"]');
const save = editor.getByRole("button", { name: "Save", exact: true });
const back = editor.getByRole("button", { name: "Back to agents", exact: true });
await expect(save).toBeVisible();
await snap(page, info, "agent-editor");
await expect(chatButtons).toHaveCount(0);
await expect(settingsWindow).toBeHidden();
for (const point of [settingsPoint, trackersPoint, await center(save), await center(back)]) {
await expect.poll(() => pressReaches(page, point, '[data-component="MobileDetailSheet"]')).toBe(true);
}

// Back returns to the Agents list, still over the chat; closing it brings the buttons back where they were.
await back.click();
await expect(editor).toHaveCount(0);
await expect(newAgent).toBeVisible();
await expect(chatButtons).toHaveCount(0);
await page.locator('[data-tour="panel-agents"]').click();
await expect(page.locator('[data-component="RightPanelMobile"]')).toHaveCount(0);
await expect(settingsButton).toBeVisible();
expect(await center(settingsButton)).toEqual(settingsPoint);
await expect(settingsWindow).toBeVisible();
await expect(settingsWindow).toHaveAttribute("data-pinned", "true");
}

test.describe("between phone and desktop widths", () => {
test.use({ viewport: { width: 856, height: 904 } });

test("chat buttons stay off other screens and under the Tracker Panel", async ({ page, request }, info) => {
test.skip(info.project.name !== "desktop-chromium", "One Chromium run covers this width.");
const chat = await createChat(request);
try {
await openChat(page, chat.id);
await checkCoveredChat(page, info);
} finally {
await request.delete(`/api/chats/${chat.id}?force=true`);
}
});
});

test.describe("in the Android app on a foldable", () => {
test.use({ viewport: { width: 856, height: 904 }, isMobile: true, hasTouch: true, userAgent: ANDROID_APP_UA });

test("chat buttons stay off other screens and under the Tracker Panel", async ({ page, request }, info) => {
test.skip(info.project.name !== "desktop-chromium", "One Chromium run covers this device.");
const chat = await createChat(request);
try {
await openChat(page, chat.id);
await checkCoveredChat(page, info);
} finally {
await request.delete(`/api/chats/${chat.id}?force=true`);
}
});
});

test.describe("on a computer", () => {
test.use({ viewport: { width: 1280, height: 800 } });

test("the Trackers button opens and closes its panel", async ({ page, request }, info) => {
test.skip(info.project.name !== "desktop-chromium", "Computer layout.");
const chat = await createChat(request);
try {
await openChat(page, chat.id);
const trackersButton = page.locator('.mari-window-bubble[data-tracker-panel-toggle="bubble"]');
const panel = page.locator('[data-component="TrackerDataSidebar"]:visible');
await expect(panel).toBeVisible();
await expect(trackersButton).toBeVisible();
await expect(trackersButton).toHaveAttribute("aria-expanded", "true");
await snap(page, info, "desktop-tracker-panel-open");

await trackersButton.click();
await expect(panel).toHaveCount(0);
await expect(trackersButton).toHaveAttribute("aria-expanded", "false");
await expect(trackersButton).toBeFocused();

await trackersButton.click();
await expect(panel).toBeVisible();
await expect(trackersButton).toHaveAttribute("aria-expanded", "true");

// Docked panels sit beside the chat, which keeps its buttons; an editor replaces the chat.
await page.locator('[data-tour="panel-agents"]').click();
const newAgent = page.locator(AGENTS_PANEL).getByRole("button", { name: "New", exact: true });
await expect(newAgent).toBeVisible();
await expect(page.locator(".mari-window-bubble[data-chat-settings-button]")).toBeVisible();
await newAgent.click();
await expect(page.getByRole("button", { name: "Save", exact: true })).toBeVisible();
await expect(page.locator(".mari-window-bubble")).toHaveCount(0);
} finally {
await request.delete(`/api/chats/${chat.id}?force=true`);
}
});
});

test("on a phone the chat buttons and Tracker Panel behave as before", async ({ page, request, isMobile }, info) => {
test.skip(!isMobile, "Phone layout.");
const chat = await createChat(request);
try {
await openChat(page, chat.id);
const trackersButton = page.locator('.mari-window-bubble[data-tracker-panel-toggle="bubble"]');
const panel = page.locator('[data-component="TrackerDataSidebar"]:visible');
// Phones begin at the button.
await expect(trackersButton).toBeVisible();
await expect(panel).toHaveCount(0);
const trackersPoint = await center(trackersButton);
await trackersButton.click();
await expect(panel).toBeVisible();
await expect.poll(() => pressReaches(page, trackersPoint, ".mari-tracker-panel")).toBe(true);
await snap(page, info, "phone-tracker-panel-open");
await panel.getByRole("button", { name: "Close tracker panel", exact: true }).click();
await expect(trackersButton).toBeVisible();
expect(await center(trackersButton)).toEqual(trackersPoint);
} finally {
await request.delete(`/api/chats/${chat.id}?force=true`);
}
});
Loading
Loading