- Deduplicate terminal IDs and favorite model slugs with trim-aware iteration - Use effect-based filtering for local branch and built-in model lists - Switch stable array handling to non-mutating `toSorted`
Showing the changes to apps (7 of 8 files). Show all
Unified · Split
| @@ -1,5 +1,7 @@ | |||
| 1 | import { resolveDefaultBranchActionDialogCopy } from "@t3tools/client-runtime"; | 1 | import { resolveDefaultBranchActionDialogCopy } from "@t3tools/client-runtime"; |
| 2 | import { resolveAutoFeatureBranchName } from "@t3tools/shared/git"; | 2 | import { resolveAutoFeatureBranchName } from "@t3tools/shared/git"; |
| 3 | import * as Arr from "effect/Array"; | ||
| 4 | import * as Result from "effect/Result"; | ||
| 3 | import { useLocalSearchParams, useRouter } from "expo-router"; | 5 | import { useLocalSearchParams, useRouter } from "expo-router"; |
| 4 | import { useCallback, useMemo } from "react"; | 6 | import { useCallback, useMemo } from "react"; |
| 5 | import { View } from "react-native"; | 7 | import { View } from "react-native"; |
| @@ -74,7 +76,9 @@ export function GitConfirmSheet() { · 68 unmodified lines | |||
| 74 | ? gitState.selectedThreadBranches | 76 | ? gitState.selectedThreadBranches |
| 75 | : await gitActions.refreshSelectedThreadBranches(); | 77 | : await gitActions.refreshSelectedThreadBranches(); |
| 76 | const newBranchName = resolveAutoFeatureBranchName( | 78 | const newBranchName = resolveAutoFeatureBranchName( |
| 77 | branches.filter((branch) => !branch.isRemote).map((branch) => branch.name), | 79 | Arr.filterMap(branches, (branch) => |
| 80 | branch.isRemote ? Result.failVoid : Result.succeed(branch.name), | ||
| 81 | ), | ||
| 78 | ); | 82 | ); |
| 79 | await gitActions.onCreateSelectedThreadBranch(newBranchName); | 83 | await gitActions.onCreateSelectedThreadBranch(newBranchName); |
| 80 | await gitActions.onRunSelectedThreadGitAction({ action: confirmAction }); | 84 | await gitActions.onRunSelectedThreadGitAction({ action: confirmAction }); |
| @@ -194,6 +194,7 @@ import { · 193 unmodified lines | |||
| 194 | isVersionMismatchDismissed, | 194 | isVersionMismatchDismissed, |
| 195 | resolveServerConfigVersionMismatch, | 195 | resolveServerConfigVersionMismatch, |
| 196 | } from "../versionSkew"; | 196 | } from "../versionSkew"; |
| 197 | import { equals } from "effect/Equal"; | ||
| 197 | 198 | ||
| 198 | const IMAGE_ONLY_BOOTSTRAP_PROMPT = | 199 | const IMAGE_ONLY_BOOTSTRAP_PROMPT = |
| 199 | "[User attached one or more images without additional text. Respond using the conversation context and the attached image(s).]"; | 200 | "[User attached one or more images without additional text. Respond using the conversation context and the attached image(s).]"; |
| @@ -438,8 +439,8 @@ function terminalIdListsEqual(left: readonly string[], right: readonly string[]) · 238 unmodified lines | |||
| 438 | if (left.length === 0) { | 439 | if (left.length === 0) { |
| 439 | return true; | 440 | return true; |
| 440 | } | 441 | } |
| 441 | const sortedLeft = [...left].sort((a, b) => a.localeCompare(b)); | 442 | const sortedLeft = left.toSorted((a, b) => a.localeCompare(b)); |
| 442 | const sortedRight = [...right].sort((a, b) => a.localeCompare(b)); | 443 | const sortedRight = right.toSorted((a, b) => a.localeCompare(b)); |
| 443 | for (let index = 0; index < sortedLeft.length; index += 1) { | 444 | for (let index = 0; index < sortedLeft.length; index += 1) { |
| 444 | if (sortedLeft[index] !== sortedRight[index]) { | 445 | if (sortedLeft[index] !== sortedRight[index]) { |
| 445 | return false; | 446 | return false; |
| @@ -870,7 +870,15 @@ export default function ThreadTerminalDrawer({ · 869 unmodified lines | |||
| 870 | const didResizeDuringDragRef = useRef(false); | 870 | const didResizeDuringDragRef = useRef(false); |
| 871 | 871 | ||
| 872 | const normalizedTerminalIds = useMemo(() => { | 872 | const normalizedTerminalIds = useMemo(() => { |
| 873 | return [...new Set(terminalIds.map((id) => id.trim()).filter((id) => id.length > 0))]; | 873 | const normalizedIds: string[] = []; |
| 874 | const seen = new Set<string>(); | ||
| 875 | for (const id of terminalIds) { | ||
| 876 | const trimmedId = id.trim(); | ||
| 877 | if (trimmedId.length === 0 || seen.has(trimmedId)) continue; | ||
| 878 | seen.add(trimmedId); | ||
| 879 | normalizedIds.push(trimmedId); | ||
| 880 | } | ||
| 881 | return normalizedIds; | ||
| 874 | }, [terminalIds]); | 882 | }, [terminalIds]); |
| 875 | 883 | ||
| 876 | const resolvedActiveTerminalId = | 884 | const resolvedActiveTerminalId = |
| @@ -904,13 +912,17 @@ export default function ThreadTerminalDrawer({ · 27 unmodified lines | |||
| 904 | }; | 912 | }; |
| 905 | 913 | ||
| 906 | for (const terminalGroup of terminalGroups) { | 914 | for (const terminalGroup of terminalGroups) { |
| 907 | const nextTerminalIds = [ | 915 | const nextTerminalIds: string[] = []; |
| 908 | ...new Set(terminalGroup.terminalIds.map((id) => id.trim()).filter((id) => id.length > 0)), | 916 | const seenGroupTerminalIds = new Set<string>(); |
| 909 | ].filter((terminalId) => { | 917 | for (const id of terminalGroup.terminalIds) { |
| 910 | if (!validTerminalIdSet.has(terminalId)) return false; | 918 | const terminalId = id.trim(); |
| 911 | if (assignedTerminalIds.has(terminalId)) return false; | 919 | if (terminalId.length === 0) continue; |
| 912 | return true; | 920 | if (seenGroupTerminalIds.has(terminalId)) continue; |
| 913 | }); | 921 | seenGroupTerminalIds.add(terminalId); |
| 922 | if (!validTerminalIdSet.has(terminalId)) continue; | ||
| 923 | if (assignedTerminalIds.has(terminalId)) continue; | ||
| 924 | nextTerminalIds.push(terminalId); | ||
| 925 | } | ||
| 914 | if (nextTerminalIds.length === 0) continue; | 926 | if (nextTerminalIds.length === 0) continue; |
| 915 | 927 | ||
| 916 | for (const terminalId of nextTerminalIds) { | 928 | for (const terminalId of nextTerminalIds) { |
| @@ -14,8 +14,10 @@ import { · 13 unmodified lines | |||
| 14 | import { scopeThreadRef } from "@t3tools/client-runtime"; | 14 | import { scopeThreadRef } from "@t3tools/client-runtime"; |
| 15 | import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings"; | 15 | import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings"; |
| 16 | import { createModelSelection } from "@t3tools/shared/model"; | 16 | import { createModelSelection } from "@t3tools/shared/model"; |
| 17 | import * as Arr from "effect/Array"; | ||
| 17 | import * as Duration from "effect/Duration"; | 18 | import * as Duration from "effect/Duration"; |
| 18 | import * as Equal from "effect/Equal"; | 19 | import * as Equal from "effect/Equal"; |
| 20 | import * as Result from "effect/Result"; | ||
| 19 | import { APP_VERSION, HOSTED_APP_CHANNEL, HOSTED_APP_CHANNEL_LABEL } from "../../branding"; | 21 | import { APP_VERSION, HOSTED_APP_CHANNEL, HOSTED_APP_CHANNEL_LABEL } from "../../branding"; |
| 20 | import { | 22 | import { |
| 21 | canCheckForUpdate, | 23 | canCheckForUpdate, |
| @@ -1143,7 +1145,12 @@ export function ProviderSettingsPanel() { · 1121 unmodified lines | |||
| 1143 | nextFavoriteModels: ReadonlyArray<string>, | 1145 | nextFavoriteModels: ReadonlyArray<string>, |
| 1144 | ) => { | 1146 | ) => { |
| 1145 | const favoriteModels = [ | 1147 | const favoriteModels = [ |
| 1146 | ...new Set(nextFavoriteModels.map((slug) => slug.trim()).filter((slug) => slug.length > 0)), | 1148 | ...new Set( |
| 1149 | Arr.filterMap(nextFavoriteModels, (slug) => { | ||
| 1150 | const trimmedSlug = slug.trim(); | ||
| 1151 | return trimmedSlug.length > 0 ? Result.succeed(trimmedSlug) : Result.failVoid; | ||
| 1152 | }), | ||
| 1153 | ), | ||
| 1147 | ]; | 1154 | ]; |
| 1148 | updateSettings({ | 1155 | updateSettings({ |
| 1149 | favorites: [ | 1156 | favorites: [ |
| @@ -1249,9 +1256,9 @@ export function ProviderSettingsPanel() { · 99 unmodified lines | |||
| 1249 | hiddenModels: [], | 1256 | hiddenModels: [], |
| 1250 | modelOrder: [], | 1257 | modelOrder: [], |
| 1251 | }; | 1258 | }; |
| 1252 | const favoriteModels = (settings.favorites ?? []) | 1259 | const favoriteModels = Arr.filterMap(settings.favorites ?? [], (favorite) => |
| 1253 | .filter((favorite) => favorite.provider === row.instanceId) | 1260 | favorite.provider === row.instanceId ? Result.succeed(favorite.model) : Result.failVoid, |
| 1254 | .map((favorite) => favorite.model); | 1261 | ); |
| 1255 | const resetLabel = driverOption?.label ?? String(row.driver); | 1262 | const resetLabel = driverOption?.label ?? String(row.driver); |
| 1256 | const headerAction = | 1263 | const headerAction = |
| 1257 | row.isDefault && row.isDirty ? ( | 1264 | row.isDefault && row.isDirty ? ( |
| @@ -204,7 +204,7 @@ export function useSourceControlActionRunning( · 203 unmodified lines | |||
| 204 | scope: SourceControlActionScope, | 204 | scope: SourceControlActionScope, |
| 205 | kinds: ReadonlyArray<SourceControlActionKind>, | 205 | kinds: ReadonlyArray<SourceControlActionKind>, |
| 206 | ): boolean { | 206 | ): boolean { |
| 207 | const stableKinds = useMemo(() => [...kinds].sort(), [kinds]); | 207 | const stableKinds = useMemo(() => kinds.toSorted(), [kinds]); |
| 208 | const appActionRunning = useSyncExternalStore( | 208 | const appActionRunning = useSyncExternalStore( |
| 209 | subscribeActionState, | 209 | subscribeActionState, |
| 210 | () => isAnyActionRunning(stableKinds, scope), | 210 | () => isAnyActionRunning(stableKinds, scope), |
| @@ -14,6 +14,8 @@ import { · 13 unmodified lines | |||
| 14 | } from "@t3tools/shared/model"; | 14 | } from "@t3tools/shared/model"; |
| 15 | import { getComposerProviderState } from "./components/chat/composerProviderState"; | 15 | import { getComposerProviderState } from "./components/chat/composerProviderState"; |
| 16 | import { UnifiedSettings } from "@t3tools/contracts/settings"; | 16 | import { UnifiedSettings } from "@t3tools/contracts/settings"; |
| 17 | import * as Arr from "effect/Array"; | ||
| 18 | import * as Result from "effect/Result"; | ||
| 17 | import { | 19 | import { |
| 18 | getDefaultServerModel, | 20 | getDefaultServerModel, |
| 19 | getProviderModels, | 21 | getProviderModels, |
| @@ -150,9 +152,9 @@ export function getAppModelOptions( · 130 unmodified lines | |||
| 150 | const options: AppModelOption[] = getProviderModels(providers, provider).map(toAppModelOption); | 152 | const options: AppModelOption[] = getProviderModels(providers, provider).map(toAppModelOption); |
| 151 | const seen = new Set(options.map((option) => option.slug)); | 153 | const seen = new Set(options.map((option) => option.slug)); |
| 152 | const builtInModelSlugs = new Set( | 154 | const builtInModelSlugs = new Set( |
| 153 | getProviderModels(providers, provider) | 155 | Arr.filterMap(getProviderModels(providers, provider), (model) => |
| 154 | .filter((model) => !model.isCustom) | 156 | model.isCustom ? Result.failVoid : Result.succeed(model.slug), |
| 155 | .map((model) => model.slug), | 157 | ), |
| 156 | ); | 158 | ); |
| 157 | 159 | ||
| 158 | // Read from the default instance's config first (that's where edits | 160 | // Read from the default instance's config first (that's where edits |
| @@ -198,7 +200,9 @@ export function getAppModelOptionsForInstance( · 39 unmodified lines | |||
| 198 | const options: AppModelOption[] = entry.models.map(toAppModelOption); | 200 | const options: AppModelOption[] = entry.models.map(toAppModelOption); |
| 199 | const seen = new Set(options.map((option) => option.slug)); | 201 | const seen = new Set(options.map((option) => option.slug)); |
| 200 | const builtInModelSlugs = new Set( | 202 | const builtInModelSlugs = new Set( |
| 201 | entry.models.filter((model) => !model.isCustom).map((model) => model.slug), | 203 | Arr.filterMap(entry.models, (model) => |
| 204 | model.isCustom ? Result.failVoid : Result.succeed(model.slug), | ||
| 205 | ), | ||
| 202 | ); | 206 | ); |
| 203 | 207 | ||
| 204 | const customModels = readInstanceCustomModels(settings, entry.instanceId, entry.driverKind); | 208 | const customModels = readInstanceCustomModels(settings, entry.instanceId, entry.driverKind); |
| @@ -58,7 +58,15 @@ function createTerminalUiStateStorage() { · 57 unmodified lines | |||
| 58 | } | 58 | } |
| 59 | 59 | ||
| 60 | function normalizeTerminalIds(terminalIds: string[]): string[] { | 60 | function normalizeTerminalIds(terminalIds: string[]): string[] { |
| 61 | return [...new Set(terminalIds.map((id) => id.trim()).filter((id) => id.length > 0))]; | 61 | const normalizedIds: string[] = []; |
| 62 | const seen = new Set<string>(); | ||
| 63 | for (const id of terminalIds) { | ||
| 64 | const trimmedId = id.trim(); | ||
| 65 | if (trimmedId.length === 0 || seen.has(trimmedId)) continue; | ||
| 66 | seen.add(trimmedId); | ||
| 67 | normalizedIds.push(trimmedId); | ||
| 68 | } | ||
| 69 | return normalizedIds; | ||
| 62 | } | 70 | } |
| 63 | 71 | ||
| 64 | function fallbackGroupId(terminalId: string): string { | 72 | function fallbackGroupId(terminalId: string): string { |
| @@ -84,7 +92,7 @@ function findGroupIndexByTerminalId( · 19 unmodified lines | |||
| 84 | } | 92 | } |
| 85 | 93 | ||
| 86 | function normalizeTerminalGroupIds(terminalIds: string[]): string[] { | 94 | function normalizeTerminalGroupIds(terminalIds: string[]): string[] { |
| 87 | return [...new Set(terminalIds.map((id) => id.trim()).filter((id) => id.length > 0))]; | 95 | return normalizeTerminalIds(terminalIds); |
| 88 | } | 96 | } |
| 89 | 97 | ||
| 90 | function normalizeTerminalGroups( | 98 | function normalizeTerminalGroups( |