History

Normalize branch and model selection handling

- 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`

Julius Marminge committed 6 months ago

Commit 7793bd61 parent 7f03f4d8 files · +63 −23Browse files

Showing the changes to apps (7 of 8 files). Show all

Unified · Split

7 files
  1. apps/mobile/src/features/threads/git/GitConfirmSheet.tsx +5 −1
  2. apps/web/src/components/ChatView.tsx +3 −2
  3. apps/web/src/components/ThreadTerminalDrawer.tsx +20 −8
  4. apps/web/src/components/settings/SettingsPanels.tsx +11 −4
  5. apps/web/src/lib/sourceControlActions.ts +1 −1
  6. apps/web/src/modelSelection.ts +8 −4
  7. apps/web/src/terminalUiStateStore.ts +10 −2
apps/mobile/src/features/threads/git/GitConfirmSheet.tsx+5−1
@@ -1,5 +1,7 @@
1import { resolveDefaultBranchActionDialogCopy } from "@t3tools/client-runtime";1import { resolveDefaultBranchActionDialogCopy } from "@t3tools/client-runtime";
2import { resolveAutoFeatureBranchName } from "@t3tools/shared/git";2import { resolveAutoFeatureBranchName } from "@t3tools/shared/git";
3import * as Arr from "effect/Array";
4import * as Result from "effect/Result";
3import { useLocalSearchParams, useRouter } from "expo-router";5import { useLocalSearchParams, useRouter } from "expo-router";
4import { useCallback, useMemo } from "react";6import { useCallback, useMemo } from "react";
5import { View } from "react-native";7import { View } from "react-native";
@@ -74,7 +76,9 @@ export function GitConfirmSheet() { · 68 unmodified lines
74 ? gitState.selectedThreadBranches76 ? 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 });
apps/web/src/components/ChatView.tsx+3−2
@@ -194,6 +194,7 @@ import { · 193 unmodified lines
194 isVersionMismatchDismissed,194 isVersionMismatchDismissed,
195 resolveServerConfigVersionMismatch,195 resolveServerConfigVersionMismatch,
196} from "../versionSkew";196} from "../versionSkew";
197import { equals } from "effect/Equal";
197 198
198const IMAGE_ONLY_BOOTSTRAP_PROMPT =199const 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;
apps/web/src/components/ThreadTerminalDrawer.tsx+20−8
@@ -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) {
apps/web/src/components/settings/SettingsPanels.tsx+11−4
@@ -14,8 +14,10 @@ import { · 13 unmodified lines
14import { scopeThreadRef } from "@t3tools/client-runtime";14import { scopeThreadRef } from "@t3tools/client-runtime";
15import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings";15import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings";
16import { createModelSelection } from "@t3tools/shared/model";16import { createModelSelection } from "@t3tools/shared/model";
17import * as Arr from "effect/Array";
17import * as Duration from "effect/Duration";18import * as Duration from "effect/Duration";
18import * as Equal from "effect/Equal";19import * as Equal from "effect/Equal";
20import * as Result from "effect/Result";
19import { APP_VERSION, HOSTED_APP_CHANNEL, HOSTED_APP_CHANNEL_LABEL } from "../../branding";21import { APP_VERSION, HOSTED_APP_CHANNEL, HOSTED_APP_CHANNEL_LABEL } from "../../branding";
20import {22import {
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 ? (
apps/web/src/lib/sourceControlActions.ts+1−1
@@ -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),
apps/web/src/modelSelection.ts+8−4
@@ -14,6 +14,8 @@ import { · 13 unmodified lines
14} from "@t3tools/shared/model";14} from "@t3tools/shared/model";
15import { getComposerProviderState } from "./components/chat/composerProviderState";15import { getComposerProviderState } from "./components/chat/composerProviderState";
16import { UnifiedSettings } from "@t3tools/contracts/settings";16import { UnifiedSettings } from "@t3tools/contracts/settings";
17import * as Arr from "effect/Array";
18import * as Result from "effect/Result";
17import {19import {
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 edits160 // 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);
apps/web/src/terminalUiStateStore.ts+10−2
@@ -58,7 +58,15 @@ function createTerminalUiStateStorage() { · 57 unmodified lines
58}58}
59 59
60function normalizeTerminalIds(terminalIds: string[]): string[] {60function 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
64function fallbackGroupId(terminalId: string): string {72function fallbackGroupId(terminalId: string): string {
@@ -84,7 +92,7 @@ function findGroupIndexByTerminalId( · 19 unmodified lines
84}92}
85 93
86function normalizeTerminalGroupIds(terminalIds: string[]): string[] {94function 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
90function normalizeTerminalGroups(98function normalizeTerminalGroups(