Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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 .agents/upstream-review.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ The cycle covers the original bound `ab099178a7b7f9728843e90fc95ed90bb61d710d` (
| Sync status timing and sorting | `ab70c8943f48c08e67e3597f62941c169bc58c16`, `6f97b0f66a51fbafabd54a5616da8827ebdf6020` | Adopted: 400ms show/minimum-visible sync status with raw phase kept for gating; precomputed sort keys shared by web and mobile. | [#935](https://github.com/pylon-code/pylon/pull/935); 289 focused tests. |
| Mobile feed, composer, Connect removal copy | `ecd3237b184e87af5d744a9dc28422cc1eb68e1a`, `dab9561cae8817bea24442c1dc7e28996de9c52e`, `ba79610d166dcc7a9b8beb013d18304bc6261307`, `525af2d1ad4b6de847ea658c37a081fd7afe8ef6`, `595a1e1f537f403eba408817c2aba35e61c2ac29` | First three adopted/adapted with Pylon Connect copy. `525af2d1ad` deferred and `595a1e1f53` not applicable: both need upstream's LegendList v2 Home (`151324b2c0`, deferred in #736). | [#928](https://github.com/pylon-code/pylon/pull/928); 184 focused tests. |
| Android usage widget, text-size scaling, native autolinks | `59abcd67ac2c54bd63a422e92fa2e31b2c667b6d`, `7a12aff471ffe2b22b9fee495b04b32c43f45a37`, `fe6388f01d5ae9f9db43079c6f5b2c31869067ca`, `81e0491d2bdec153bf1ee12d4e1deab3909b7428` | Widget adapted (scrolling list on 12L+, older Android keeps the fitted layout). Scaling partial: Pylon sizing helper only; `Material*` hunks need `e106fe95cc` (skipped as a Pylon product choice in #736). Autolink patch adopted on registry 0.5.8 — the `deps/` tarball override is npm-only and unused by pnpm; needs a native build. | [#930](https://github.com/pylon-code/pylon/pull/930), [#933](https://github.com/pylon-code/pylon/pull/933); 403 mobile tests; Kotlin not compiled locally. |
| Per-thread auto-settle switch | `0109670411d13b9e380766e98b386b2f756ecf5b` | Adapted: migration 067, `threadAutoSettleOptOut` capability, default unchanged on replay. | [#931](https://github.com/pylon-code/pylon/pull/931); 87 server tests, six typechecks. |
| Per-thread auto-settle switch | `0109670411d13b9e380766e98b386b2f756ecf5b` | Adapted: migration 067, `threadAutoSettleOptOut` capability, default unchanged on replay. This follow-up adds the capability-gated switch to Pylon’s retained legacy mobile Home/sidebar and v2 fallback menus; native menu evidence remains pending. | [#931](https://github.com/pylon-code/pylon/pull/931); 87 server tests, six typechecks. |
| node-pty upgrade, Windows terminal startup, Linux URL handler | `4923ff417f15fe1930fbb679e548706b55f7cde8`, `ea7d46ac107e6a3461b81065ebc86dfd5483095f`, `b528a701102f95089544596856a3f208f4404613`, `b21f3b71913370a1a650a61fd9082f992d40e657`, `7733bc839e23e19467a70b11a0bcedf6ffe8f1a3` | node-pty chain adopted; prebuild validation already covered (test added). URL handler partial: MIME cache refresh adopted, icon copy excluded because Pylon installs a per-channel icon. | [#929](https://github.com/pylon-code/pylon/pull/929), [#932](https://github.com/pylon-code/pylon/pull/932); Windows not run locally. |
| Bitbucket credentials | `5da55957d48dbd79c1a05b72bdf2479ae0956b1c` | Adapted: stored in `ServerSecretStore`, redacted to clients, `bitbucketCredentials` capability, unusable tokens refused on save. | [#936](https://github.com/pylon-code/pylon/pull/936); 112 focused tests plus RPC redaction test. |
| Transcript usage and OpenCode Go limits | `adfc9240eab7c5db376a87591b497d8144289257`, `1a553d0f5dd2aa39a64895ec770dc51ef1448b1a` | Adopted; credential fingerprint uses Pylon's JSON key format. | [#940](https://github.com/pylon-code/pylon/pull/940); 132 focused tests. |
Expand Down
22 changes: 13 additions & 9 deletions apps/mobile/src/features/home/HomeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ import {
} from "../threads/threadListV2";
import { useThreadListV2ShelfPreferences } from "../threads/use-thread-list-v2-shelf-preferences";
import type { HomeListFilterMenuEnvironment } from "./home-list-filter-menu";
import { useLegacyHomeListExtraData } from "./home-list-extra-data";
import {
buildHomeListLayout,
DEFAULT_GROUP_DISPLAY_STATE,
Expand Down Expand Up @@ -1034,15 +1035,14 @@ export function HomeScreen(props: HomeScreenProps) {
],
);

const extraData = useMemo(
() => ({
projectCwdByKey,
savedConnectionsById: props.savedConnectionsById,
searchQuery: props.searchQuery,
threadSearchMatchByKey,
}),
[projectCwdByKey, props.savedConnectionsById, props.searchQuery, threadSearchMatchByKey],
);
const extraData = useLegacyHomeListExtraData({
projectCwdByKey,
savedConnectionsById: props.savedConnectionsById,
searchQuery: props.searchQuery,
threadSearchMatchByKey,
autoSettleOptOutEnvironmentIds,
titleRegenerationEnvironmentIds,
});

const renderItem = useCallback(
({ item }: LegendListRenderItemProps<HomeListItem>) => {
Expand Down Expand Up @@ -1110,6 +1110,8 @@ export function HomeScreen(props: HomeScreenProps) {
onRenameThread={handleRenameThread}
onRegenerateThreadTitle={handleRegenerateThreadTitle}
titleRegenerationSupported={titleRegenerationEnvironmentIds.has(thread.environmentId)}
autoSettleOptOutSupported={autoSettleOptOutEnvironmentIds.has(thread.environmentId)}
onSetThreadAutoSettle={handleSetThreadAutoSettle}
onSelectThread={props.onSelectThread}
onSwipeableClose={handleSwipeableClose}
onSwipeableWillOpen={handleSwipeableWillOpen}
Expand All @@ -1132,6 +1134,8 @@ export function HomeScreen(props: HomeScreenProps) {
handleSwipeableClose,
handleSwipeableWillOpen,
handleRegenerateThreadTitle,
handleSetThreadAutoSettle,
autoSettleOptOutEnvironmentIds,
handleRenameThread,
machineByEnvironmentId,
projectCwdByKey,
Expand Down
62 changes: 62 additions & 0 deletions apps/mobile/src/features/home/home-list-extra-data.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import type { EnvironmentThreadSearchMatch } from "@t3tools/client-runtime/state/thread-search";
import { EnvironmentId } from "@t3tools/contracts";
import { act, create, type ReactTestRenderer } from "react-test-renderer";
import { describe, expect, it } from "vite-plus/test";

import { useLegacyHomeListExtraData } from "./home-list-extra-data";

Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true });

type Input = Parameters<typeof useLegacyHomeListExtraData>[0];

const environmentId = EnvironmentId.make("environment-a");
const baseInput: Input = {
projectCwdByKey: new Map<string, string>(),
savedConnectionsById: {},
searchQuery: "",
threadSearchMatchByKey: new Map<string, EnvironmentThreadSearchMatch>(),
autoSettleOptOutEnvironmentIds: new Set<EnvironmentId>(),
titleRegenerationEnvironmentIds: new Set<EnvironmentId>(),
};

function renderExtraData(initial: Input) {
const seen: unknown[] = [];
function Probe(props: { readonly input: Input }) {
seen.push(useLegacyHomeListExtraData(props.input));
return null;
}
let renderer: ReactTestRenderer | undefined;
act(() => {
renderer = create(<Probe input={initial} />);
});
return {
seen,
rerender(input: Input) {
act(() => renderer?.update(<Probe input={input} />));
},
};
}

describe("useLegacyHomeListExtraData", () => {
it("keeps its identity across renders with unchanged inputs", () => {
const probe = renderExtraData(baseInput);
probe.rerender({ ...baseInput });
expect(probe.seen).toHaveLength(2);
expect(probe.seen[1]).toBe(probe.seen[0]);
});

// A recycled row only re-renders on an item or extraData change, so a
// capability that lands (server configs after threads) or disappears
// (server downgrade) must change the identity or the row menu goes stale.
it("changes identity when the auto-settle capability set changes", () => {
const probe = renderExtraData(baseInput);
probe.rerender({ ...baseInput, autoSettleOptOutEnvironmentIds: new Set([environmentId]) });
expect(probe.seen[1]).not.toBe(probe.seen[0]);
});

it("changes identity when the title regeneration capability set changes", () => {
const probe = renderExtraData(baseInput);
probe.rerender({ ...baseInput, titleRegenerationEnvironmentIds: new Set([environmentId]) });
expect(probe.seen[1]).not.toBe(probe.seen[0]);
});
});
49 changes: 49 additions & 0 deletions apps/mobile/src/features/home/home-list-extra-data.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import type { EnvironmentThreadSearchMatch } from "@t3tools/client-runtime/state/thread-search";
import type { EnvironmentId } from "@t3tools/contracts";
import { useMemo } from "react";

import type { SavedRemoteConnection } from "../../lib/connection";

/**
* The legacy Home list's `extraData`. A recycled LegendList cell re-renders
* only when its item changes or this identity changes — a new render closure
* alone does not — so every non-item input a row reads must be listed here.
* The capability sets gate row menu items (title regeneration, auto-settle)
* and arrive with server configs, which can land after the threads or change
* when a server is upgraded or downgraded.
*/
export function useLegacyHomeListExtraData(input: {
readonly projectCwdByKey: ReadonlyMap<string, string>;
readonly savedConnectionsById: Readonly<Record<string, SavedRemoteConnection>>;
readonly searchQuery: string;
readonly threadSearchMatchByKey: ReadonlyMap<string, EnvironmentThreadSearchMatch>;
readonly autoSettleOptOutEnvironmentIds: ReadonlySet<EnvironmentId>;
readonly titleRegenerationEnvironmentIds: ReadonlySet<EnvironmentId>;
}) {
const {
projectCwdByKey,
savedConnectionsById,
searchQuery,
threadSearchMatchByKey,
autoSettleOptOutEnvironmentIds,
titleRegenerationEnvironmentIds,
} = input;
return useMemo(
() => ({
projectCwdByKey,
savedConnectionsById,
searchQuery,
threadSearchMatchByKey,
autoSettleOptOutEnvironmentIds,
titleRegenerationEnvironmentIds,
}),
[
projectCwdByKey,
savedConnectionsById,
searchQuery,
threadSearchMatchByKey,
autoSettleOptOutEnvironmentIds,
titleRegenerationEnvironmentIds,
],
);
}
2 changes: 2 additions & 0 deletions apps/mobile/src/features/threads/ThreadNavigationSidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1105,6 +1105,8 @@ function ThreadNavigationSidebarPane(
onRenameThread={renameThread}
onRegenerateThreadTitle={regenerateThreadTitle}
titleRegenerationSupported={titleRegenerationEnvironmentIds.has(thread.environmentId)}
autoSettleOptOutSupported={autoSettleOptOutEnvironmentIds.has(thread.environmentId)}
onSetThreadAutoSettle={setThreadAutoSettle}
onSelectThread={handleSelectThread}
onSwipeableClose={handleSwipeableClose}
onSwipeableWillOpen={handleSwipeableWillOpen}
Expand Down
28 changes: 28 additions & 0 deletions apps/mobile/src/features/threads/thread-auto-settle-menu.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import type { MenuAction } from "@react-native-menu/menu";

export function buildThreadAutoSettleMenuItems(input: {
readonly supported: boolean;
readonly autoSettleDisabledAt: string | null | undefined;
}): MenuAction[] {
if (!input.supported) return [];

return [
{
id: "auto-settle",
title: "Auto-settle behavior",
image: "timer",
subactions: [
{
id: "auto-settle:enabled",
title: "Enabled",
state: input.autoSettleDisabledAt == null ? "on" : "off",
},
{
id: "auto-settle:disabled",
title: "Disabled",
state: input.autoSettleDisabledAt == null ? "off" : "on",
},
],
},
];
}
Loading
Loading