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 | 56 | export function dispatchHardwareKeyboardCommand(command: HardwareKeyboardCommand): boolean { |
| 57 | 57 | const commandHandlers = handlers.get(command); |
| 58 | 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 | 63 | if (handler() !== false) return true; |
| 61 | 64 | } |
| 62 | 65 | return false; |
| @@ -8,7 +8,8 @@ import { · 7 unmodified lines | ||
| 8 | 8 | } from "@t3tools/client-runtime/state/thread-relationships"; |
| 9 | 9 | import { canDetachThreadProviderSession } from "@t3tools/client-runtime/state/thread-workflows"; |
| 10 | 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 | 13 | import { useMemo, useState } from "react"; |
| 13 | 14 | import { ActivityIndicator, Modal, Pressable, ScrollView, View } from "react-native"; |
| 14 | 15 | import { useSafeAreaInsets } from "react-native-safe-area-context"; |
| @@ -79,7 +80,8 @@ export function ThreadRelationshipsBanner(props: { · 64 unmodified lines | ||
| 79 | 80 | const mergeTargetThreadId = resolveMergeBackTargetThreadId(projection); |
| 80 | 81 | const rows = useMemo( |
| 81 | 82 | () => |
| 82 | − immediateThreadRelationships(graph, props.threadId).toSorted( | |
| 83 | + copySorted( | |
| 84 | + immediateThreadRelationships(graph, props.threadId), | |
| 83 | 85 | (left, right) => |
| 84 | 86 | Number(right.threadId === mergeTargetThreadId) - |
| 85 | 87 | Number(left.threadId === mergeTargetThreadId), |
| @@ -9,6 +9,7 @@ import { · 8 unmodified lines | ||
| 9 | 9 | } from "@t3tools/contracts"; |
| 10 | 10 | import type { EnvironmentThreadShell } from "@t3tools/client-runtime/state/shell"; |
| 11 | 11 | import * as Option from "effect/Option"; |
| 12 | +import { copySorted } from "@t3tools/shared/Array"; | |
| 12 | 13 | |
| 13 | 14 | import { useProject, useThreadShell } from "../state/entities"; |
| 14 | 15 | import { useEnvironmentThread } from "../state/threads"; |
| @@ -44,8 +45,21 @@ function threadDetailToShell( · 29 unmodified lines | ||
| 44 | 45 | environmentId: EnvironmentId, |
| 45 | 46 | thread: OrchestrationThread, |
| 46 | 47 | ): 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, { | |
| 49 | 63 | id: thread.id, |
| 50 | 64 | projectId: thread.projectId, |
| 51 | 65 | title: thread.title, |
| @@ -3,6 +3,7 @@ import type { · 2 unmodified lines | ||
| 3 | 3 | OrchestrationV2ProviderCapabilities, |
| 4 | 4 | OrchestrationV2ThreadProjection, |
| 5 | 5 | } from "@t3tools/contracts"; |
| 6 | +import { copySorted } from "@t3tools/shared/Array"; | |
| 6 | 7 | |
| 7 | 8 | type Projection = OrchestrationV2ThreadProjection; |
| 8 | 9 | type Run = Projection["runs"][number]; |
| @@ -54,19 +55,17 @@ export function deriveThreadQueueWorkflowState(projection: Projection): ThreadQu · 45 unmodified lines | ||
| 54 | 55 | const activeRun = resolveActiveThreadRun(projection); |
| 55 | 56 | const session = resolveThreadProviderSession(projection); |
| 56 | 57 | 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 | + })); | |
| 70 | 69 | |
| 71 | 70 | return { |
| 72 | 71 | activeRun, |
| @@ -3,6 +3,10 @@ · 2 unmodified lines | ||
| 3 | 3 | "private": true, |
| 4 | 4 | "type": "module", |
| 5 | 5 | "exports": { |
| 6 | + "./Array": { | |
| 7 | + "types": "./src/Array.ts", | |
| 8 | + "import": "./src/Array.ts" | |
| 9 | + }, | |
| 6 | 10 | "./themePalettes": { |
| 7 | 11 | "types": "./src/themePalettes.ts", |
| 8 | 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 | 8 | type ProviderOptionDescriptor, |
| 9 | 9 | type ProviderOptionSelection, |
| 10 | 10 | } from "@t3tools/contracts"; |
| 11 | −import * as Option from "effect/Option"; | |
| 12 | −import * as Schema from "effect/Schema"; | |
| 11 | +import { copySorted } from "./Array.ts"; | |
| 13 | 12 | |
| 14 | 13 | const DEFAULT_PROVIDER_DRIVER_KIND = ProviderDriverKind.make("codex"); |
| 15 | 14 | |
| @@ -75,12 +74,21 @@ export function getModelSelectionBooleanOptionValue( · 59 unmodified lines | ||
| 75 | 74 | function canonicalModelSelectionOptions( |
| 76 | 75 | modelSelection: ModelSelection, |
| 77 | 76 | ): 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 | + ) => { | |
| 81 | 88 | const idOrder = leftId.localeCompare(rightId); |
| 82 | 89 | return idOrder !== 0 ? idOrder : String(leftValue).localeCompare(String(rightValue)); |
| 83 | − }); | |
| 90 | + }, | |
| 91 | + ); | |
| 84 | 92 | } |
| 85 | 93 | |
| 86 | 94 | /** |