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 fs = require("node:fs");
const path = require("node:path"); const path = require("node:path");
const pkg = require("./package.json"); const pkg = require("./package.json");
@@ -142,6 +143,7 @@ export default {
searchPaths: ["../../node_modules", "./node_modules"], searchPaths: ["../../node_modules", "./node_modules"],
}, },
plugins: [ plugins: [
withAndroidOrientation,
"expo-router", "expo-router",
withPasteInput, withPasteInput,
withAndroidScroll, withAndroidScroll,
+28
View File
@@ -59,3 +59,31 @@ describe("Android app config", () => {
30_000, 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( const service = bindTestSessionHost(
new PluginService(pino({ level: "silent" }), store, "0.4.0"), new PluginService(pino({ level: "silent" }), store, "0.4.0"),
); );
try { await service.start();
await service.start(); await service.installDirectory({ path: first });
await service.installDirectory({ path: first }); await service.installDirectory({ path: second });
await service.installDirectory({ path: second });
expect(catalogIds(service)).toEqual(["first", "second"]);
await store.patch({ pluginsEnabled: false });
expect(service.catalog()).toEqual([]); await store.patch({ pluginsEnabled: false });
await expect(service.invokePluginRpc("second", "anything", {})).rejects.toThrow(
"Plugin is not available", expect(service.catalog()).toEqual([]);
); await expect(service.invokePluginRpc("second", "anything", {})).rejects.toThrow(
await store.patch({ pluginsEnabled: true }); "Plugin is not available",
// Observe recovery itself. Reloading here restarts a healthy plugin and );
// would also conceal a failure to restart it after the global enable. await store.patch({ pluginsEnabled: true });
await expect await service.reloadPlugin("first");
.poll(() => service.listPlugins(), { timeout: 10_000 }) expect((await service.listPlugins()).map(({ id, status }) => ({ id, status }))).toEqual([
.toMatchObject([ { id: "first", status: "running" },
{ id: "first", status: "running" }, { id: "second", status: "running" },
{ id: "second", status: "running" }, ]);
]); await service.stopAllPlugins();
} finally {
await service.stopAllPlugins();
}
}, 20_000); }, 20_000);
it("does not publish an in-flight start after a later global disable", async () => { 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. 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 ## Prerequisites
@@ -40,15 +40,13 @@ This is analysis only. Do NOT edit, create, or delete any files. Do NOT write co
## Forwarded skills ## 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` → advisor runs `/unslop` on the current diff.
- `/paseo-advisor /unslop-risk` → advisor does an unslop-risk review. - `/paseo-advisor /unslop-risk` → advisor does an unslop-risk review.
- `/paseo-advisor $diagnose this build failure` → advisor invokes `/diagnose`. - `/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. 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. 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 ## 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. 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 ## 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. 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 ## 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. 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 ## Check current documentation