History

fix(mobile): support Hermes collection sorting

Reuses the focused compatibility approach from closed PR #3923 and covers the remaining hardware-keyboard reverse iteration.

Julius Marminge committed 3 months ago

Commit cd2556b1 parent 6c344388 files · +72 −24Browse files

Unified · Split

8 files
  1. apps/mobile/src/features/keyboard/hardwareKeyboardCommands.ts +4 −1
  2. apps/mobile/src/features/threads/ThreadRelationshipsBanner.tsx +4 −2
  3. apps/mobile/src/state/use-thread-selection.ts +16 −2
  4. packages/client-runtime/src/state/threadWorkflows.ts +12 −13
  5. packages/shared/package.json +4 −0
  6. packages/shared/src/Array.test.ts +12 −0
  7. packages/shared/src/Array.ts +6 −0
  8. packages/shared/src/model.ts +14 −6
apps/mobile/src/features/keyboard/hardwareKeyboardCommands.ts+4−1
@@ -56,7 +56,10 @@ export function subscribeToHardwareKeyboardCommandRegistrations(listener: () => · 55 unmodified lines
5656 export function dispatchHardwareKeyboardCommand(command: HardwareKeyboardCommand): boolean {
5757 const commandHandlers = handlers.get(command);
5858 if (!commandHandlers) return false;
59− for (const handler of [...commandHandlers].toReversed()) {
59+ const handlersInRegistrationOrder = Array.from(commandHandlers);
60+ for (let index = handlersInRegistrationOrder.length - 1; index >= 0; index -= 1) {
61+ const handler = handlersInRegistrationOrder[index];
62+ if (!handler) continue;
6063 if (handler() !== false) return true;
6164 }
6265 return false;
apps/mobile/src/features/threads/ThreadRelationshipsBanner.tsx+4−2
@@ -8,7 +8,8 @@ import { · 7 unmodified lines
88 } from "@t3tools/client-runtime/state/thread-relationships";
99 import { canDetachThreadProviderSession } from "@t3tools/client-runtime/state/thread-workflows";
1010 import type { EnvironmentId, OrchestrationV2ThreadShell, ThreadId } from "@t3tools/contracts";
11−import { useRouter } from "expo-router";
11+import { copySorted } from "@t3tools/shared/Array";
12+import { useNavigation } from "@react-navigation/native";
1213 import { useMemo, useState } from "react";
1314 import { ActivityIndicator, Modal, Pressable, ScrollView, View } from "react-native";
1415 import { useSafeAreaInsets } from "react-native-safe-area-context";
@@ -79,7 +80,8 @@ export function ThreadRelationshipsBanner(props: { · 64 unmodified lines
7980 const mergeTargetThreadId = resolveMergeBackTargetThreadId(projection);
8081 const rows = useMemo(
8182 () =>
82− immediateThreadRelationships(graph, props.threadId).toSorted(
83+ copySorted(
84+ immediateThreadRelationships(graph, props.threadId),
8385 (left, right) =>
8486 Number(right.threadId === mergeTargetThreadId) -
8587 Number(left.threadId === mergeTargetThreadId),
apps/mobile/src/state/use-thread-selection.ts+16−2
@@ -9,6 +9,7 @@ import { · 8 unmodified lines
99 } from "@t3tools/contracts";
1010 import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell";
1111 import * as Option from "effect/Option";
12+import { copySorted } from "@t3tools/shared/Array";
1213
1314 import { useProject, useThreadShell } from "../state/entities";
1415 import { useEnvironmentThread } from "../state/threads";
@@ -44,8 +45,21 @@ function threadDetailToShell( · 29 unmodified lines
4445 environmentId: EnvironmentId,
4546 thread: OrchestrationThread,
4647 ): EnvironmentThreadShell {
47− return {
48− environmentId,
48+ const thread = projection.thread;
49+ const runsByOrdinal = copySorted(projection.runs, (left, right) => right.ordinal - left.ordinal);
50+ const latestRun = runsByOrdinal[0] ?? null;
51+ const activeRun =
52+ runsByOrdinal.find(
53+ (run) =>
54+ run.status === "preparing" ||
55+ run.status === "queued" ||
56+ run.status === "starting" ||
57+ run.status === "running" ||
58+ run.status === "waiting",
59+ ) ?? null;
60+ const pendingRequest =
61+ projection.runtimeRequests.find((request) => request.status === "pending") ?? null;
62+ return presentThreadShell(environmentId, {
4963 id: thread.id,
5064 projectId: thread.projectId,
5165 title: thread.title,
packages/client-runtime/src/state/threadWorkflows.ts+12−13
@@ -3,6 +3,7 @@ import type { · 2 unmodified lines
33 OrchestrationV2ProviderCapabilities,
44 OrchestrationV2ThreadProjection,
55 } from "@t3tools/contracts";
6+import { copySorted } from "@t3tools/shared/Array";
67
78 type Projection = OrchestrationV2ThreadProjection;
89 type Run = Projection["runs"][number];
@@ -54,19 +55,17 @@ export function deriveThreadQueueWorkflowState(projection: Projection): ThreadQu · 45 unmodified lines
5455 const activeRun = resolveActiveThreadRun(projection);
5556 const session = resolveThreadProviderSession(projection);
5657 const capabilities = session?.capabilities.turns;
57− const queuedRuns = projection.runs
58− .filter((run) => run.status === "queued")
59− .toSorted(
60− (left, right) =>
61− (left.queuePosition ?? left.ordinal) - (right.queuePosition ?? right.ordinal) ||
62− left.ordinal - right.ordinal,
63− )
64− .map((run) => ({
65− run,
66− text:
67− projection.messages.find((message) => message.id === run.userMessageId)?.text ??
68− "Queued message",
69− }));
58+ const queuedRuns = copySorted(
59+ projection.runs.filter((run) => run.status === "queued"),
60+ (left, right) =>
61+ (left.queuePosition ?? left.ordinal) - (right.queuePosition ?? right.ordinal) ||
62+ left.ordinal - right.ordinal,
63+ ).map((run) => ({
64+ run,
65+ text:
66+ projection.messages.find((message) => message.id === run.userMessageId)?.text ??
67+ "Queued message",
68+ }));
7069
7170 return {
7271 activeRun,
packages/shared/package.json+4−0
@@ -3,6 +3,10 @@ · 2 unmodified lines
33 "private": true,
44 "type": "module",
55 "exports": {
6+ "./Array": {
7+ "types": "./src/Array.ts",
8+ "import": "./src/Array.ts"
9+ },
610 "./themePalettes": {
711 "types": "./src/themePalettes.ts",
812 "import": "./src/themePalettes.ts"
packages/shared/src/Array.test.ts+12−0
@@ -0,0 +1,12 @@
1+import { describe, expect, it } from "vite-plus/test";
2+
3+import { copySorted } from "./Array.ts";
4+
5+describe("array copies", () => {
6+ it("sorts a copy without mutating the source", () => {
7+ const source = [3, 1, 2];
8+
9+ expect(copySorted(source, (left: number, right: number) => left - right)).toEqual([1, 2, 3]);
10+ expect(source).toEqual([3, 1, 2]);
11+ });
12+});
packages/shared/src/Array.ts+6−0
@@ -0,0 +1,6 @@
1+export function copySorted<T>(
2+ values: ReadonlyArray<T>,
3+ compareFn?: (left: T, right: T) => number,
4+): T[] {
5+ return [...values].sort(compareFn);
6+}
packages/shared/src/model.ts+14−6
@@ -8,8 +8,7 @@ import { · 7 unmodified lines
88 type ProviderOptionDescriptor,
99 type ProviderOptionSelection,
1010 } from "@t3tools/contracts";
11−import * as Option from "effect/Option";
12−import * as Schema from "effect/Schema";
11+import { copySorted } from "./Array.ts";
1312
1413 const DEFAULT_PROVIDER_DRIVER_KIND = ProviderDriverKind.make("codex");
1514
@@ -75,12 +74,21 @@ export function getModelSelectionBooleanOptionValue( · 59 unmodified lines
7574 function canonicalModelSelectionOptions(
7675 modelSelection: ModelSelection,
7776 ): ReadonlyArray<readonly [id: string, value: string | boolean]> {
78− return (modelSelection.options ?? [])
79− .map((selection) => [selection.id, selection.value] as const)
80− .toSorted(([leftId, leftValue], [rightId, rightValue]) => {
77+ return copySorted(
78+ (modelSelection.options ?? []).map(
79+ (selection): readonly [id: string, value: string | boolean] => [
80+ selection.id,
81+ selection.value,
82+ ],
83+ ),
84+ (
85+ [leftId, leftValue]: readonly [id: string, value: string | boolean],
86+ [rightId, rightValue]: readonly [id: string, value: string | boolean],
87+ ) => {
8188 const idOrder = leftId.localeCompare(rightId);
8289 return idOrder !== 0 ? idOrder : String(leftValue).localeCompare(String(rightValue));
83− });
90+ },
91+ );
8492 }
8593
8694 /**

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t