Compare commits

..
Author SHA1 Message Date
Mohamed Boudra 15635531c6 fix(app): keep tablet orientation policy in activity
Android resolves manifest resources without screen size qualifiers, so preserve the portrait declaration and unlock tablets when the activity starts.
2026-10-30 03:50:14 +01:00
b678e122d5 fix(app): allow Android tablets to rotate
Choose the orientation before the first activity frame, and update it when the available screen size changes.

Co-authored-by: yz <43207690+yzim@users.noreply.github.com>
Co-authored-by: magiclaw <930198090@qq.com>
2026-10-30 03:42:37 +01:00
9 changed files with 91 additions and 31 deletions
+2
View File
@@ -1,3 +1,4 @@
const withAndroidOrientation = require("./plugins/with-android-orientation");
const fs = require("node:fs");
const path = require("node:path");
const pkg = require("./package.json");
@@ -142,6 +143,7 @@ export default {
searchPaths: ["../../node_modules", "./node_modules"],
},
plugins: [
withAndroidOrientation,
"expo-router",
withPasteInput,
withAndroidScroll,
+28
View File
@@ -59,3 +59,31 @@ describe("Android app config", () => {
30_000,
);
});
it("keeps the Android manifest portrait for phone startup", async () => {
const manifest = await resolveAndroidManifest();
const activity = manifest.application[0].activity.find(
(entry: ManifestEntry) => entry.$["android:name"] === ".MainActivity",
);
expect(activity.$["android:screenOrientation"]).toBe("portrait");
}, 30_000);
it("updates the native orientation before launch and when screen size changes", async () => {
const plugin = createRequire(import.meta.url)("./plugins/with-android-orientation.js");
const config = plugin({ name: "test", slug: "test" });
const result = await config.mods.android.mainActivity({
modRequest: {},
modResults: {
language: "kt",
contents:
"class MainActivity {\n override fun onCreate(savedInstanceState: Bundle?) {\n super.onCreate(null)\n }\n}",
},
});
expect(result.modResults.contents).toContain("smallestScreenWidthDp >= 600");
expect(result.modResults.contents).toContain("SCREEN_ORIENTATION_UNSPECIFIED");
expect(result.modResults.contents).toContain("SCREEN_ORIENTATION_PORTRAIT");
expect(result.modResults.contents).toContain(
"updateOrientation(resources.configuration)\n super.onCreate(null)",
);
expect(result.modResults.contents).toContain("updateOrientation(newConfig)");
});
@@ -0,0 +1,38 @@
const { withMainActivity } = require("expo/config-plugins");
module.exports = function withAndroidOrientation(config) {
return withMainActivity(config, (mod) => {
const activity = mod.modResults;
if (activity.language !== "kt") throw new Error("Expected Kotlin MainActivity");
const marker = " // Paseo orientation policy";
if (!activity.contents.includes(marker)) {
const anchor = " override fun onCreate(savedInstanceState: Bundle?)";
if (!activity.contents.includes(anchor)) throw new Error("MainActivity anchor missing");
activity.contents = activity.contents.replace(
anchor,
`${marker}
private fun updateOrientation(configuration: android.content.res.Configuration) {
val orientation = if (configuration.smallestScreenWidthDp >= 600)
android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
else
android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
if (requestedOrientation != orientation) requestedOrientation = orientation
}
override fun onConfigurationChanged(newConfig: android.content.res.Configuration) {
super.onConfigurationChanged(newConfig)
updateOrientation(newConfig)
}
${anchor}`,
);
if (!activity.contents.includes(" super.onCreate(null)"))
throw new Error("MainActivity onCreate missing");
activity.contents = activity.contents.replace(
" super.onCreate(null)",
" updateOrientation(resources.configuration)\n super.onCreate(null)",
);
}
return mod;
});
};
@@ -1075,29 +1075,23 @@ export default function contribute(plugin: unknown) {
const service = bindTestSessionHost(
new PluginService(pino({ level: "silent" }), store, "0.4.0"),
);
try {
await service.start();
await service.installDirectory({ path: first });
await service.installDirectory({ path: second });
expect(catalogIds(service)).toEqual(["first", "second"]);
await store.patch({ pluginsEnabled: false });
await service.start();
await service.installDirectory({ path: first });
await service.installDirectory({ path: second });
expect(service.catalog()).toEqual([]);
await expect(service.invokePluginRpc("second", "anything", {})).rejects.toThrow(
"Plugin is not available",
);
await store.patch({ pluginsEnabled: true });
// Observe recovery itself. Reloading here restarts a healthy plugin and
// would also conceal a failure to restart it after the global enable.
await expect
.poll(() => service.listPlugins(), { timeout: 10_000 })
.toMatchObject([
{ id: "first", status: "running" },
{ id: "second", status: "running" },
]);
} finally {
await service.stopAllPlugins();
}
await store.patch({ pluginsEnabled: false });
expect(service.catalog()).toEqual([]);
await expect(service.invokePluginRpc("second", "anything", {})).rejects.toThrow(
"Plugin is not available",
);
await store.patch({ pluginsEnabled: true });
await service.reloadPlugin("first");
expect((await service.listPlugins()).map(({ id, status }) => ({ id, status }))).toEqual([
{ id: "first", status: "running" },
{ id: "second", status: "running" },
]);
await service.stopAllPlugins();
}, 20_000);
it("does not publish an in-flight start after a later global disable", async () => {
+3 -5
View File
@@ -9,7 +9,7 @@ argument-hint: "[--profile <name>] <question or topic>"
Single agent. Reads the situation you're in. Gives a judgment. You decide what to do — the advisor doesn't drive the work.
Use the user's request and current conversation as input, including any arguments supplied with the skill invocation. Optional syntax: `[--profile <name>] <question or topic>`.
**User's request:** $ARGUMENTS
## Prerequisites
@@ -40,15 +40,13 @@ This is analysis only. Do NOT edit, create, or delete any files. Do NOT write co
## Forwarded skills
If the user's request contains another skill reference, such as `/unslop`, `/skill:unslop`, or `$unslop`, the user is asking the advisor to run that skill against the current task. Examples:
If `$ARGUMENTS` contains another skill reference — `/unslop`, `/unslop-risk`, `$unslop`, etc. — the user is asking the advisor to run that skill against the current task. Examples:
- `/paseo-advisor /unslop` → advisor runs `/unslop` on the current diff.
- `/paseo-advisor /unslop-risk` → advisor does an unslop-risk review.
- `/paseo-advisor $diagnose this build failure` → advisor invokes `/diagnose`.
- `/skill:paseo-advisor /skill:unslop` → advisor runs the `unslop` skill on the current diff in Pi.
- `$paseo-advisor $unslop` → advisor runs the `unslop` skill on the current diff in Codex.
Parse the forwarded skill name from the user's request (`/<name>`, `/skill:<name>`, or `$<name>`), excluding this skill's own invocation. In the briefing, tell the advisor explicitly:
Parse the forwarded skill name out of `$ARGUMENTS` (`/<name>` or `$<name>`). In the briefing, tell the advisor explicitly:
```
Invoke the `<name>` skill against this task. Load it via the Skill tool before doing anything else.
+1 -1
View File
@@ -8,7 +8,7 @@ user-invocable: true
Two agents from contrasting profiles, fresh context, planning a solution in parallel.
Use the user's request and current conversation as input, including any arguments supplied with the skill invocation.
**User's additional context:** $ARGUMENTS
## Prerequisites
+1 -1
View File
@@ -8,7 +8,7 @@ user-invocable: true
Transfer the current task — context, decisions, failed attempts, constraints — to a fresh agent. The receiving agent starts with **zero context**, so the handoff prompt must be a self-contained briefing.
Use the user's request and current conversation as input, including any arguments supplied with the skill invocation.
**User's arguments:** $ARGUMENTS
## Prerequisites
+1 -1
View File
@@ -7,7 +7,7 @@ description: Answer questions about the Paseo product and app, including setup,
You are helping a user understand, configure, or troubleshoot Paseo itself. Answer their question directly, verify the answer against the current public documentation, and include the relevant documentation link. Do not send the user away to read the docs in place of helping them.
Use the user's question and current conversation as input, including any arguments supplied with the skill invocation.
**User's question:** $ARGUMENTS
## Use current documentation
+1 -1
View File
@@ -7,7 +7,7 @@ description: Build and manage trusted local Paseo plugins. Use when the user ask
Build or manage the requested plugin directly. Use the current public docs to catch contract changes, but keep working from this skill if the network is unavailable.
Use the user's request and current conversation as input, including any arguments supplied with the skill invocation.
**User's request:** $ARGUMENTS
## Check current documentation