Reuses the focused compatibility approach from closed PR #3923 and covers the remaining hardware-keyboard reverse iteration.
Unified · Split
| @@ -56,7 +56,10 @@ export function subscribeToHardwareKeyboardCommandRegistrations(listener: () => · 55 unmodified lines | |||
| 56 | export function dispatchHardwareKeyboardCommand(command: HardwareKeyboardCommand): boolean { | 56 | export function dispatchHardwareKeyboardCommand(command: HardwareKeyboardCommand): boolean { |
| 57 | const commandHandlers = handlers.get(command); | 57 | const commandHandlers = handlers.get(command); |
| 58 | if (!commandHandlers) return false; | 58 | 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; | ||
| 60 | if (handler() !== false) return true; | 63 | if (handler() !== false) return true; |
| 61 | } | 64 | } |
| 62 | return false; | 65 | return false; |
| @@ -8,7 +8,8 @@ import { · 7 unmodified lines | |||
| 8 | } from "@t3tools/client-runtime/state/thread-relationships"; | 8 | } from "@t3tools/client-runtime/state/thread-relationships"; |
| 9 | import { canDetachThreadProviderSession } from "@t3tools/client-runtime/state/thread-workflows"; | 9 | import { canDetachThreadProviderSession } from "@t3tools/client-runtime/state/thread-workflows"; |
| 10 | import type { EnvironmentId, OrchestrationV2ThreadShell, ThreadId } from "@t3tools/contracts"; | 10 | 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"; | ||
| 12 | import { useMemo, useState } from "react"; | 13 | import { useMemo, useState } from "react"; |
| 13 | import { ActivityIndicator, Modal, Pressable, ScrollView, View } from "react-native"; | 14 | import { ActivityIndicator, Modal, Pressable, ScrollView, View } from "react-native"; |
| 14 | import { useSafeAreaInsets } from "react-native-safe-area-context"; | 15 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| @@ -79,7 +80,8 @@ export function ThreadRelationshipsBanner(props: { · 64 unmodified lines | |||
| 79 | const mergeTargetThreadId = resolveMergeBackTargetThreadId(projection); | 80 | const mergeTargetThreadId = resolveMergeBackTargetThreadId(projection); |
| 80 | const rows = useMemo( | 81 | const rows = useMemo( |
| 81 | () => | 82 | () => |
| 82 | immediateThreadRelationships(graph, props.threadId).toSorted( | 83 | copySorted( |
| 84 | immediateThreadRelationships(graph, props.threadId), | ||
| 83 | (left, right) => | 85 | (left, right) => |
| 84 | Number(right.threadId === mergeTargetThreadId) - | 86 | Number(right.threadId === mergeTargetThreadId) - |
| 85 | Number(left.threadId === mergeTargetThreadId), | 87 | Number(left.threadId === mergeTargetThreadId), |
| @@ -9,6 +9,7 @@ import { · 8 unmodified lines | |||
| 9 | } from "@t3tools/contracts"; | 9 | } from "@t3tools/contracts"; |
| 10 | import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; | 10 | import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; |
| 11 | import * as Option from "effect/Option"; | 11 | import * as Option from "effect/Option"; |
| 12 | import { copySorted } from "@t3tools/shared/Array"; | ||
| 12 | 13 | ||
| 13 | import { useProject, useThreadShell } from "../state/entities"; | 14 | import { useProject, useThreadShell } from "../state/entities"; |
| 14 | import { useEnvironmentThread } from "../state/threads"; | 15 | import { useEnvironmentThread } from "../state/threads"; |
| @@ -44,8 +45,21 @@ function threadDetailToShell( · 29 unmodified lines | |||
| 44 | environmentId: EnvironmentId, | 45 | environmentId: EnvironmentId, |
| 45 | thread: OrchestrationThread, | 46 | thread: OrchestrationThread, |
| 46 | ): EnvironmentThreadShell { | 47 | ): EnvironmentThreadShell { |
| 47 | return { | 48 | const thread = projection.thread; |
| 48 | environmentId, | 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, { | ||
| 49 | id: thread.id, | 63 | id: thread.id, |
| 50 | projectId: thread.projectId, | 64 | projectId: thread.projectId, |
| 51 | title: thread.title, | 65 | title: thread.title, |
| @@ -3,6 +3,7 @@ import type { · 2 unmodified lines | |||
| 3 | OrchestrationV2ProviderCapabilities, | 3 | OrchestrationV2ProviderCapabilities, |
| 4 | OrchestrationV2ThreadProjection, | 4 | OrchestrationV2ThreadProjection, |
| 5 | } from "@t3tools/contracts"; | 5 | } from "@t3tools/contracts"; |
| 6 | import { copySorted } from "@t3tools/shared/Array"; | ||
| 6 | 7 | ||
| 7 | type Projection = OrchestrationV2ThreadProjection; | 8 | type Projection = OrchestrationV2ThreadProjection; |
| 8 | type Run = Projection["runs"][number]; | 9 | type Run = Projection["runs"][number]; |
| @@ -54,19 +55,17 @@ export function deriveThreadQueueWorkflowState(projection: Projection): ThreadQu · 45 unmodified lines | |||
| 54 | const activeRun = resolveActiveThreadRun(projection); | 55 | const activeRun = resolveActiveThreadRun(projection); |
| 55 | const session = resolveThreadProviderSession(projection); | 56 | const session = resolveThreadProviderSession(projection); |
| 56 | const capabilities = session?.capabilities.turns; | 57 | const capabilities = session?.capabilities.turns; |
| 57 | const queuedRuns = projection.runs | 58 | const queuedRuns = copySorted( |
| 58 | .filter((run) => run.status === "queued") | 59 | projection.runs.filter((run) => run.status === "queued"), |
| 59 | .toSorted( | 60 | (left, right) => |
| 60 | (left, right) => | 61 | (left.queuePosition ?? left.ordinal) - (right.queuePosition ?? right.ordinal) || |
| 61 | (left.queuePosition ?? left.ordinal) - (right.queuePosition ?? right.ordinal) || | 62 | left.ordinal - right.ordinal, |
| 62 | left.ordinal - right.ordinal, | 63 | ).map((run) => ({ |
| 63 | ) | 64 | run, |
| 64 | .map((run) => ({ | 65 | text: |
| 65 | run, | 66 | projection.messages.find((message) => message.id === run.userMessageId)?.text ?? |
| 66 | text: | 67 | "Queued message", |
| 67 | projection.messages.find((message) => message.id === run.userMessageId)?.text ?? | 68 | })); |
| 68 | "Queued message", | ||
| 69 | })); | ||
| 70 | 69 | ||
| 71 | return { | 70 | return { |
| 72 | activeRun, | 71 | activeRun, |
| @@ -3,6 +3,10 @@ · 2 unmodified lines | |||
| 3 | "private": true, | 3 | "private": true, |
| 4 | "type": "module", | 4 | "type": "module", |
| 5 | "exports": { | 5 | "exports": { |
| 6 | "./Array": { | ||
| 7 | "types": "./src/Array.ts", | ||
| 8 | "import": "./src/Array.ts" | ||
| 9 | }, | ||
| 6 | "./themePalettes": { | 10 | "./themePalettes": { |
| 7 | "types": "./src/themePalettes.ts", | 11 | "types": "./src/themePalettes.ts", |
| 8 | "import": "./src/themePalettes.ts" | 12 | "import": "./src/themePalettes.ts" |
| @@ -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 | }); | ||
| @@ -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 | } | ||
| @@ -8,8 +8,7 @@ import { · 7 unmodified lines | |||
| 8 | type ProviderOptionDescriptor, | 8 | type ProviderOptionDescriptor, |
| 9 | type ProviderOptionSelection, | 9 | type ProviderOptionSelection, |
| 10 | } from "@t3tools/contracts"; | 10 | } from "@t3tools/contracts"; |
| 11 | import * as Option from "effect/Option"; | 11 | import { copySorted } from "./Array.ts"; |
| 12 | import * as Schema from "effect/Schema"; | ||
| 13 | 12 | ||
| 14 | const DEFAULT_PROVIDER_DRIVER_KIND = ProviderDriverKind.make("codex"); | 13 | const DEFAULT_PROVIDER_DRIVER_KIND = ProviderDriverKind.make("codex"); |
| 15 | 14 | ||
| @@ -75,12 +74,21 @@ export function getModelSelectionBooleanOptionValue( · 59 unmodified lines | |||
| 75 | function canonicalModelSelectionOptions( | 74 | function canonicalModelSelectionOptions( |
| 76 | modelSelection: ModelSelection, | 75 | modelSelection: ModelSelection, |
| 77 | ): ReadonlyArray<readonly [id: string, value: string | boolean]> { | 76 | ): ReadonlyArray<readonly [id: string, value: string | boolean]> { |
| 78 | return (modelSelection.options ?? []) | 77 | return copySorted( |
| 79 | .map((selection) => [selection.id, selection.value] as const) | 78 | (modelSelection.options ?? []).map( |
| 80 | .toSorted(([leftId, leftValue], [rightId, rightValue]) => { | 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 | ) => { | ||
| 81 | const idOrder = leftId.localeCompare(rightId); | 88 | const idOrder = leftId.localeCompare(rightId); |
| 82 | return idOrder !== 0 ? idOrder : String(leftValue).localeCompare(String(rightValue)); | 89 | return idOrder !== 0 ? idOrder : String(leftValue).localeCompare(String(rightValue)); |
| 83 | }); | 90 | }, |
| 91 | ); | ||
| 84 | } | 92 | } |
| 85 | 93 | ||
| 86 | /** | 94 | /** |