authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-05-28 16:18:58-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-05-28 16:18:58-07:00
log31268945f88a712fad9309d5ea9290cb8496af83
treef10d21c963f7de25996020995052981f7b0f1aee
parent6b3050ee740f510ad06c12a91ecd7e338792dce0
signature Signed by PGP key B5690EEEBB952194

Extract collection performance refactors from mobile stack (#2854)

Co-authored-by: codex <codex@users.noreply.github.com>

47 files changed, 646 insertions(+), 455 deletions(-)

apps/server/src/auth/Layers/AuthControlPlane.ts+16-10
...@@ -64,11 +64,16 @@ export const makeAuthControlPlane = Effect.gen(function* () {...@@ -64,11 +64,16 @@ export const makeAuthControlPlane = Effect.gen(function* () {
6464
65 const listPairingLinks: AuthControlPlaneShape["listPairingLinks"] = (input) =>65 const listPairingLinks: AuthControlPlaneShape["listPairingLinks"] = (input) =>
66 bootstrapCredentials.listActive().pipe(66 bootstrapCredentials.listActive().pipe(
67 Effect.map((pairingLinks) =>67 Effect.map((pairingLinks) => {
68 pairingLinks68 const activeLinks: Array<AuthPairingLink> = [];
69 .filter((pairingLink) => (input?.role ? pairingLink.role === input.role : true))69 for (const pairingLink of pairingLinks) {
70 .filter((pairingLink) => !input?.excludeSubjects?.includes(pairingLink.subject))70 if (input?.role && pairingLink.role !== input.role) {
71 .map((pairingLink) =>71 continue;
72 }
73 if (input?.excludeSubjects?.includes(pairingLink.subject)) {
74 continue;
75 }
76 activeLinks.push(
72 pairingLink.label77 pairingLink.label
73 ? ({78 ? ({
74 id: pairingLink.id,79 id: pairingLink.id,
...@@ -87,11 +92,12 @@ export const makeAuthControlPlane = Effect.gen(function* () {...@@ -87,11 +92,12 @@ export const makeAuthControlPlane = Effect.gen(function* () {
87 createdAt: pairingLink.createdAt,92 createdAt: pairingLink.createdAt,
88 expiresAt: pairingLink.expiresAt,93 expiresAt: pairingLink.expiresAt,
89 } satisfies AuthPairingLink),94 } satisfies AuthPairingLink),
90 )95 );
91 .toSorted(96 }
92 (left, right) => right.createdAt.epochMilliseconds - left.createdAt.epochMilliseconds,97 return activeLinks.toSorted(
93 ),98 (left, right) => right.createdAt.epochMilliseconds - left.createdAt.epochMilliseconds,
94 ),99 );
100 }),
95 Effect.mapError(toAuthControlPlaneError("Failed to list pairing links.")),101 Effect.mapError(toAuthControlPlaneError("Failed to list pairing links.")),
96 );102 );
97103
apps/server/src/git/remoteRefs.ts+8-4
...@@ -1,8 +1,12 @@...@@ -1,8 +1,12 @@
1export function parseRemoteNamesInGitOrder(stdout: string): ReadonlyArray<string> {1export function parseRemoteNamesInGitOrder(stdout: string): ReadonlyArray<string> {
2 return stdout2 const remoteNames: Array<string> = [];
3 .split("\n")3 for (const line of stdout.split("\n")) {
4 .map((line) => line.trim())4 const remoteName = line.trim();
5 .filter((line) => line.length > 0);5 if (remoteName.length > 0) {
6 remoteNames.push(remoteName);
7 }
8 }
9 return remoteNames;
6}10}
711
8export function parseRemoteNames(stdout: string): ReadonlyArray<string> {12export function parseRemoteNames(stdout: string): ReadonlyArray<string> {
apps/server/src/imageMime.ts+7-4
...@@ -35,10 +35,13 @@ export function parseBase64DataUrl(...@@ -35,10 +35,13 @@ export function parseBase64DataUrl(
35 const match = /^data:([^,]+),([a-z0-9+/=\r\n ]+)$/i.exec(dataUrl.trim());35 const match = /^data:([^,]+),([a-z0-9+/=\r\n ]+)$/i.exec(dataUrl.trim());
36 if (!match) return null;36 if (!match) return null;
3737
38 const headerParts = (match[1] ?? "")38 const headerParts: Array<string> = [];
39 .split(";")39 for (const part of (match[1] ?? "").split(";")) {
40 .map((part) => part.trim())40 const trimmed = part.trim();
41 .filter((part) => part.length > 0);41 if (trimmed.length > 0) {
42 headerParts.push(trimmed);
43 }
44 }
42 if (headerParts.length < 2) {45 if (headerParts.length < 2) {
43 return null;46 return null;
44 }47 }
apps/server/src/keybindings.ts+6-3
...@@ -32,6 +32,7 @@ import * as Layer from "effect/Layer";...@@ -32,6 +32,7 @@ import * as Layer from "effect/Layer";
32import * as Option from "effect/Option";32import * as Option from "effect/Option";
33import * as Predicate from "effect/Predicate";33import * as Predicate from "effect/Predicate";
34import * as PubSub from "effect/PubSub";34import * as PubSub from "effect/PubSub";
35import * as Result from "effect/Result";
35import * as Schema from "effect/Schema";36import * as Schema from "effect/Schema";
36import * as SchemaIssue from "effect/SchemaIssue";37import * as SchemaIssue from "effect/SchemaIssue";
37import * as SchemaTransformation from "effect/SchemaTransformation";38import * as SchemaTransformation from "effect/SchemaTransformation";
...@@ -538,9 +539,11 @@ const makeKeybindings = Effect.gen(function* () {...@@ -538,9 +539,11 @@ const makeKeybindings = Effect.gen(function* () {
538 return;539 return;
539 }540 }
540541
541 const matchingDefaults = DEFAULT_KEYBINDINGS.filter((defaultRule) =>542 const matchingDefaults = Array.filterMap(DEFAULT_KEYBINDINGS, (defaultRule) =>
542 customConfig.some((entry) => isSameKeybindingRule(entry, defaultRule)),543 customConfig.some((entry) => isSameKeybindingRule(entry, defaultRule))
543 ).map((rule) => rule.command);544 ? Result.succeed(defaultRule.command)
545 : Result.failVoid,
546 );
544 if (matchingDefaults.length > 0) {547 if (matchingDefaults.length > 0) {
545 yield* Effect.logWarning("default keybinding rule already defined in user config", {548 yield* Effect.logWarning("default keybinding rule already defined in user config", {
546 path: keybindingsConfigPath,549 path: keybindingsConfigPath,
apps/server/src/orchestration/Layers/CheckpointReactor.ts+7-3
...@@ -1,5 +1,6 @@...@@ -1,5 +1,6 @@
1import {1import {
2 CommandId,2 CommandId,
3 type CheckpointRef,
3 EventId,4 EventId,
4 MessageId,5 MessageId,
5 type ProjectId,6 type ProjectId,
...@@ -701,9 +702,12 @@ const make = Effect.gen(function* () {...@@ -701,9 +702,12 @@ const make = Effect.gen(function* () {
701 });702 });
702 }703 }
703704
704 const staleCheckpointRefs = thread.checkpoints705 const staleCheckpointRefs: Array<CheckpointRef> = [];
705 .filter((checkpoint) => checkpoint.checkpointTurnCount > event.payload.turnCount)706 for (const checkpoint of thread.checkpoints) {
706 .map((checkpoint) => checkpoint.checkpointRef);707 if (checkpoint.checkpointTurnCount > event.payload.turnCount) {
708 staleCheckpointRefs.push(checkpoint.checkpointRef);
709 }
710 }
707711
708 if (staleCheckpointRefs.length > 0) {712 if (staleCheckpointRefs.length > 0) {
709 yield* checkpointStore.deleteCheckpointRefs({713 yield* checkpointStore.deleteCheckpointRefs({
apps/server/src/orchestration/Layers/ProjectionPipeline.ts+9-6
...@@ -537,12 +537,15 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti...@@ -537,12 +537,15 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
537 projectionPendingApprovalRepository.listByThreadId({ threadId }),537 projectionPendingApprovalRepository.listByThreadId({ threadId }),
538 ]);538 ]);
539539
540 const latestUserMessageAt =540 let latestUserMessageAt: string | null = null;
541 messages541 for (const message of messages) {
542 .filter((message) => message.role === "user")542 if (
543 .map((message) => message.createdAt)543 message.role === "user" &&
544 .toSorted()544 (latestUserMessageAt === null || message.createdAt > latestUserMessageAt)
545 .at(-1) ?? null;545 ) {
546 latestUserMessageAt = message.createdAt;
547 }
548 }
546549
547 const pendingApprovalCount = pendingApprovals.filter(550 const pendingApprovalCount = pendingApprovals.filter(
548 (approval) => approval.status === "pending",551 (approval) => approval.status === "pending",
apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts+39-33
...@@ -24,9 +24,11 @@ import {...@@ -24,9 +24,11 @@ import {
24 ProjectId,24 ProjectId,
25 ThreadId,25 ThreadId,
26} from "@t3tools/contracts";26} from "@t3tools/contracts";
27import * as Arr from "effect/Array";
27import * as Effect from "effect/Effect";28import * as Effect from "effect/Effect";
28import * as Layer from "effect/Layer";29import * as Layer from "effect/Layer";
29import * as Option from "effect/Option";30import * as Option from "effect/Option";
31import * as Result from "effect/Result";
30import * as Schema from "effect/Schema";32import * as Schema from "effect/Schema";
31import * as Struct from "effect/Struct";33import * as Struct from "effect/Struct";
32import * as SqlClient from "effect/unstable/sql/SqlClient";34import * as SqlClient from "effect/unstable/sql/SqlClient";
...@@ -1488,34 +1490,36 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {...@@ -1488,34 +1490,36 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
14881490
1489 const snapshot = {1491 const snapshot = {
1490 snapshotSequence: computeSnapshotSequence(stateRows),1492 snapshotSequence: computeSnapshotSequence(stateRows),
1491 projects: projectRows1493 projects: Arr.filterMap(projectRows, (row) =>
1492 .filter((row) => row.deletedAt === null)1494 row.deletedAt === null
1493 .map((row) =>1495 ? Result.succeed(
1494 mapProjectShellRow(row, repositoryIdentities.get(row.projectId) ?? null),1496 mapProjectShellRow(row, repositoryIdentities.get(row.projectId) ?? null),
1495 ),1497 )
1496 threads: threadRows1498 : Result.failVoid,
1497 .filter((row) => row.deletedAt === null)1499 ),
1498 .map(1500 threads: Arr.filterMap(threadRows, (row) =>
1499 (row): OrchestrationThreadShell => ({1501 row.deletedAt === null
1500 id: row.threadId,1502 ? Result.succeed({
1501 projectId: row.projectId,1503 id: row.threadId,
1502 title: row.title,1504 projectId: row.projectId,
1503 modelSelection: row.modelSelection,1505 title: row.title,
1504 runtimeMode: row.runtimeMode,1506 modelSelection: row.modelSelection,
1505 interactionMode: row.interactionMode,1507 runtimeMode: row.runtimeMode,
1506 branch: row.branch,1508 interactionMode: row.interactionMode,
1507 worktreePath: row.worktreePath,1509 branch: row.branch,
1508 latestTurn: latestTurnByThread.get(row.threadId) ?? null,1510 worktreePath: row.worktreePath,
1509 createdAt: row.createdAt,1511 latestTurn: latestTurnByThread.get(row.threadId) ?? null,
1510 updatedAt: row.updatedAt,1512 createdAt: row.createdAt,
1511 archivedAt: row.archivedAt,1513 updatedAt: row.updatedAt,
1512 session: sessionByThread.get(row.threadId) ?? null,1514 archivedAt: row.archivedAt,
1513 latestUserMessageAt: row.latestUserMessageAt,1515 session: sessionByThread.get(row.threadId) ?? null,
1514 hasPendingApprovals: row.pendingApprovalCount > 0,1516 latestUserMessageAt: row.latestUserMessageAt,
1515 hasPendingUserInput: row.pendingUserInputCount > 0,1517 hasPendingApprovals: row.pendingApprovalCount > 0,
1516 hasActionableProposedPlan: row.hasActionableProposedPlan > 0,1518 hasPendingUserInput: row.pendingUserInputCount > 0,
1517 }),1519 hasActionableProposedPlan: row.hasActionableProposedPlan > 0,
1518 ),1520 } satisfies OrchestrationThreadShell)
1521 : Result.failVoid,
1522 ),
1519 updatedAt: updatedAt ?? "1970-01-01T00:00:00.000Z",1523 updatedAt: updatedAt ?? "1970-01-01T00:00:00.000Z",
1520 };1524 };
15211525
...@@ -1621,11 +1625,13 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {...@@ -1621,11 +1625,13 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
16211625
1622 const snapshot = {1626 const snapshot = {
1623 snapshotSequence: computeSnapshotSequence(stateRows),1627 snapshotSequence: computeSnapshotSequence(stateRows),
1624 projects: projectRows1628 projects: Arr.filterMap(projectRows, (row) =>
1625 .filter((row) => row.deletedAt === null && activeProjectIds.has(row.projectId))1629 row.deletedAt === null && activeProjectIds.has(row.projectId)
1626 .map((row) =>1630 ? Result.succeed(
1627 mapProjectShellRow(row, repositoryIdentities.get(row.projectId) ?? null),1631 mapProjectShellRow(row, repositoryIdentities.get(row.projectId) ?? null),
1628 ),1632 )
1633 : Result.failVoid,
1634 ),
1629 threads: threadRows.map(1635 threads: threadRows.map(
1630 (row): OrchestrationThreadShell => ({1636 (row): OrchestrationThreadShell => ({
1631 id: row.threadId,1637 id: row.threadId,
apps/server/src/provider/Layers/ClaudeAdapter.ts+7-3
...@@ -245,9 +245,13 @@ function toProcessError(...@@ -245,9 +245,13 @@ function toProcessError(
245function normalizeClaudeStreamMessages(245function normalizeClaudeStreamMessages(
246 cause: Cause.Cause<{ readonly message: string }>,246 cause: Cause.Cause<{ readonly message: string }>,
247): ReadonlyArray<string> {247): ReadonlyArray<string> {
248 const errors = Cause.prettyErrors(cause)248 const errors: Array<string> = [];
249 .map((error) => error.message.trim())249 for (const error of Cause.prettyErrors(cause)) {
250 .filter((message) => message.length > 0);250 const message = error.message.trim();
251 if (message.length > 0) {
252 errors.push(message);
253 }
254 }
251 if (errors.length > 0) {255 if (errors.length > 0) {
252 return errors;256 return errors;
253 }257 }
apps/server/src/provider/Layers/ClaudeProvider.ts+7-5
...@@ -317,11 +317,13 @@ export function resolveClaudeApiModelId(modelSelection: ModelSelection): string...@@ -317,11 +317,13 @@ export function resolveClaudeApiModelId(modelSelection: ModelSelection): string
317}317}
318318
319function toTitleCaseWords(value: string): string {319function toTitleCaseWords(value: string): string {
320 return value320 const parts: Array<string> = [];
321 .split(/[\s_-]+/g)321 for (const part of value.split(/[\s_-]+/g)) {
322 .filter(Boolean)322 if (part.length > 0) {
323 .map((part) => part[0]!.toUpperCase() + part.slice(1).toLowerCase())323 parts.push(part[0]!.toUpperCase() + part.slice(1).toLowerCase());
324 .join(" ");324 }
325 }
326 return parts.join(" ");
325}327}
326328
327function claudeSubscriptionLabel(subscriptionType: string | undefined): string | undefined {329function claudeSubscriptionLabel(subscriptionType: string | undefined): string | undefined {
apps/server/src/provider/Layers/CodexProvider.ts+15-10
...@@ -319,16 +319,21 @@ const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(fun...@@ -319,16 +319,21 @@ const probeCodexAppServerProvider = Effect.fn("probeCodexAppServerProvider")(fun
319 } satisfies CodexAppServerProviderSnapshot;319 } satisfies CodexAppServerProviderSnapshot;
320});320});
321321
322const emptyCodexModelsFromSettings = (codexSettings: CodexSettings): ServerProvider["models"] =>322const emptyCodexModelsFromSettings = (codexSettings: CodexSettings): ServerProvider["models"] => {
323 codexSettings.customModels323 const models = new Set<string>();
324 .map((model) => model.trim())324 for (const model of codexSettings.customModels) {
325 .filter((model, index, models) => model.length > 0 && models.indexOf(model) === index)325 const trimmed = model.trim();
326 .map((model) => ({326 if (trimmed.length > 0) {
327 slug: model,327 models.add(trimmed);
328 name: model,328 }
329 isCustom: true,329 }
330 capabilities: null,330 return Array.from(models, (model) => ({
331 }));331 slug: model,
332 name: model,
333 isCustom: true,
334 capabilities: null,
335 }));
336};
332337
333const makePendingCodexProvider = (338const makePendingCodexProvider = (
334 codexSettings: CodexSettings,339 codexSettings: CodexSettings,
apps/server/src/provider/Layers/CursorProvider.ts+20-13
...@@ -596,11 +596,12 @@ export const discoverCursorModelCapabilitiesViaAcp = (...@@ -596,11 +596,12 @@ export const discoverCursorModelCapabilitiesViaAcp = (
596 );596 );
597 }597 }
598598
599 const targetModelSlugs = new Set(599 const targetModelSlugs = new Set<string>();
600 existingModels600 for (const model of existingModels) {
601 .filter((model) => !model.isCustom && !hasCursorModelCapabilities(model))601 if (!model.isCustom && !hasCursorModelCapabilities(model)) {
602 .map((model) => model.slug),602 targetModelSlugs.add(model.slug);
603 );603 }
604 }
604 if (targetModelSlugs.size === 0) {605 if (targetModelSlugs.size === 0) {
605 return buildCursorDiscoveredModels(606 return buildCursorDiscoveredModels(
606 modelChoices.map((modelChoice) => ({607 modelChoices.map((modelChoice) => ({
...@@ -721,9 +722,13 @@ export interface CursorAboutResult {...@@ -721,9 +722,13 @@ export interface CursorAboutResult {
721}722}
722723
723function joinProviderMessages(...messages: ReadonlyArray<string | undefined>): string | undefined {724function joinProviderMessages(...messages: ReadonlyArray<string | undefined>): string | undefined {
724 const parts = messages725 const parts: Array<string> = [];
725 .map((message) => message?.trim())726 for (const message of messages) {
726 .filter((message): message is string => Boolean(message));727 const trimmed = message?.trim();
728 if (trimmed) {
729 parts.push(trimmed);
730 }
731 }
727 return parts.length > 0 ? parts.join(" ") : undefined;732 return parts.length > 0 ? parts.join(" ") : undefined;
728}733}
729734
...@@ -790,11 +795,13 @@ export function parseCursorCliConfigChannel(raw: string): string | undefined {...@@ -790,11 +795,13 @@ export function parseCursorCliConfigChannel(raw: string): string | undefined {
790}795}
791796
792function toTitleCaseWords(value: string): string {797function toTitleCaseWords(value: string): string {
793 return value798 const parts: Array<string> = [];
794 .split(/[\s_-]+/g)799 for (const part of value.split(/[\s_-]+/g)) {
795 .filter((part) => part.length > 0)800 if (part.length > 0) {
796 .map((part) => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())801 parts.push(part.charAt(0).toUpperCase() + part.slice(1).toLowerCase());
797 .join(" ");802 }
803 }
804 return parts.join(" ");
798}805}
799806
800function cursorSubscriptionLabel(subscriptionType: string | undefined): string | undefined {807function cursorSubscriptionLabel(subscriptionType: string | undefined): string | undefined {
apps/server/src/provider/Layers/OpenCodeAdapter.ts+9-6
...@@ -1366,12 +1366,15 @@ export function makeOpenCodeAdapter(...@@ -1366,12 +1366,15 @@ export function makeOpenCodeAdapter(
1366 }),1366 }),
1367 ).pipe(Effect.mapError(toRequestError));1367 ).pipe(Effect.mapError(toRequestError));
13681368
1369 const turns = (messages.data ?? [])1369 const turns: Array<OpenCodeTurnSnapshot> = [];
1370 .filter((entry) => entry.info.role === "assistant")1370 for (const entry of messages.data ?? []) {
1371 .map((entry) => ({1371 if (entry.info.role === "assistant") {
1372 id: TurnId.make(entry.info.id),1372 turns.push({
1373 items: [entry.info, ...entry.parts],1373 id: TurnId.make(entry.info.id),
1374 }));1374 items: [entry.info, ...entry.parts],
1375 });
1376 }
1377 }
13751378
1376 return {1379 return {
1377 threadId,1380 threadId,
apps/server/src/provider/Layers/OpenCodeProvider.ts+7-5
...@@ -137,11 +137,13 @@ function formatOpenCodeProbeError(input: {...@@ -137,11 +137,13 @@ function formatOpenCodeProbeError(input: {
137}137}
138138
139function titleCaseSlug(value: string): string {139function titleCaseSlug(value: string): string {
140 return value140 const segments: Array<string> = [];
141 .split(/[-_/]+/)141 for (const segment of value.split(/[-_/]+/)) {
142 .filter((segment) => segment.length > 0)142 if (segment.length > 0) {
143 .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))143 segments.push(segment.charAt(0).toUpperCase() + segment.slice(1));
144 .join(" ");144 }
145 }
146 return segments.join(" ");
145}147}
146148
147function inferDefaultVariant(149function inferDefaultVariant(
apps/server/src/provider/acp/AcpRuntimeModel.ts+36-27
...@@ -126,19 +126,20 @@ export function parseSessionModeState(...@@ -126,19 +126,20 @@ export function parseSessionModeState(
126 if (!currentModeId) {126 if (!currentModeId) {
127 return undefined;127 return undefined;
128 }128 }
129 const availableModes = modes.availableModes129 const availableModes: Array<AcpSessionMode> = [];
130 .map((mode) => {130 for (const mode of modes.availableModes) {
131 const id = mode.id.trim();131 const id = mode.id.trim();
132 const name = mode.name.trim();132 const name = mode.name.trim();
133 if (!id || !name) {133 if (!id || !name) {
134 return undefined;134 continue;
135 }135 }
136 const description = mode.description?.trim() || undefined;136 const description = mode.description?.trim() || undefined;
137 return description !== undefined137 availableModes.push(
138 description !== undefined
138 ? ({ id, name, description } satisfies AcpSessionMode)139 ? ({ id, name, description } satisfies AcpSessionMode)
139 : ({ id, name } satisfies AcpSessionMode);140 : ({ id, name } satisfies AcpSessionMode),
140 })141 );
141 .filter((mode): mode is AcpSessionMode => mode !== undefined);142 }
142 if (availableModes.length === 0) {143 if (availableModes.length === 0) {
143 return undefined;144 return undefined;
144 }145 }
...@@ -186,9 +187,15 @@ function normalizeCommandValue(value: unknown): string | undefined {...@@ -186,9 +187,15 @@ function normalizeCommandValue(value: unknown): string | undefined {
186 if (!Array.isArray(value)) {187 if (!Array.isArray(value)) {
187 return undefined;188 return undefined;
188 }189 }
189 const parts = value190 const parts: Array<string> = [];
190 .map((entry) => (typeof entry === "string" && entry.trim().length > 0 ? entry.trim() : null))191 for (const entry of value) {
191 .filter((entry): entry is string => entry !== null);192 if (typeof entry === "string") {
193 const part = entry.trim();
194 if (part.length > 0) {
195 parts.push(part);
196 }
197 }
198 }
192 return parts.length > 0 ? parts.join(" ") : undefined;199 return parts.length > 0 ? parts.join(" ") : undefined;
193}200}
194201
...@@ -222,18 +229,20 @@ function extractTextContentFromToolCallContent(...@@ -222,18 +229,20 @@ function extractTextContentFromToolCallContent(
222 content: ReadonlyArray<EffectAcpSchema.ToolCallContent> | null | undefined,229 content: ReadonlyArray<EffectAcpSchema.ToolCallContent> | null | undefined,
223): string | undefined {230): string | undefined {
224 if (!content) return undefined;231 if (!content) return undefined;
225 const chunks = content232 const chunks: Array<string> = [];
226 .map((entry) => {233 for (const entry of content) {
227 if (entry.type !== "content") {234 if (entry.type !== "content") {
228 return undefined;235 continue;
229 }236 }
230 const nestedContent = entry.content;237 const nestedContent = entry.content;
231 if (nestedContent.type !== "text") {238 if (nestedContent.type !== "text") {
232 return undefined;239 continue;
233 }240 }
234 return nestedContent.text.trim().length > 0 ? nestedContent.text.trim() : undefined;241 const text = nestedContent.text.trim();
235 })242 if (text.length > 0) {
236 .filter((entry): entry is string => entry !== undefined);243 chunks.push(text);
244 }
245 }
237 return chunks.length > 0 ? chunks.join("\n") : undefined;246 return chunks.length > 0 ? chunks.join("\n") : undefined;
238}247}
239248
apps/server/src/provider/providerMaintenanceRunner.ts+9-7
...@@ -214,13 +214,15 @@ export const make = Effect.fn("ProviderMaintenanceRunner.make")(function* () {...@@ -214,13 +214,15 @@ export const make = Effect.fn("ProviderMaintenanceRunner.make")(function* () {
214 instanceId: ProviderInstanceId,214 instanceId: ProviderInstanceId,
215 ): Effect.Effect<VerifiedProviderRefresh> =>215 ): Effect.Effect<VerifiedProviderRefresh> =>
216 providerRegistry.getProviders.pipe(216 providerRegistry.getProviders.pipe(
217 Effect.map((providers) =>217 Effect.map((providers) => {
218 providers218 const instanceIds: Array<ProviderInstanceId> = [];
219 .filter(219 for (const candidate of providers) {
220 (candidate) => candidate.driver === provider && candidate.instanceId === instanceId,220 if (candidate.driver === provider && candidate.instanceId === instanceId) {
221 )221 instanceIds.push(candidate.instanceId);
222 .map((candidate) => candidate.instanceId),222 }
223 ),223 }
224 return instanceIds;
225 }),
224 Effect.flatMap((instanceIds) =>226 Effect.flatMap((instanceIds) =>
225 instanceIds.length === 0227 instanceIds.length === 0
226 ? providerRegistry.refreshInstance(instanceId)228 ? providerRegistry.refreshInstance(instanceId)
apps/server/src/sourceControl/AzureDevOpsCli.ts+7-4
...@@ -208,10 +208,13 @@ function parseRepositorySpecifier(repository: string): {...@@ -208,10 +208,13 @@ function parseRepositorySpecifier(repository: string): {
208 readonly project: string | null;208 readonly project: string | null;
209 readonly name: string;209 readonly name: string;
210} {210} {
211 const parts = repository211 const parts: Array<string> = [];
212 .split("/")212 for (const part of repository.split("/")) {
213 .map((part) => part.trim())213 const trimmed = part.trim();
214 .filter((part) => part.length > 0);214 if (trimmed.length > 0) {
215 parts.push(trimmed);
216 }
217 }
215 return {218 return {
216 project: parts.length > 1 ? (parts.at(-2) ?? null) : null,219 project: parts.length > 1 ? (parts.at(-2) ?? null) : null,
217 name: parts.at(-1) ?? repository.trim(),220 name: parts.at(-1) ?? repository.trim(),
apps/server/src/sourceControl/GitLabCli.ts+7-4
...@@ -247,10 +247,13 @@ function parseRepositoryPath(repository: string): {...@@ -247,10 +247,13 @@ function parseRepositoryPath(repository: string): {
247 readonly namespacePath: string | null;247 readonly namespacePath: string | null;
248 readonly projectPath: string;248 readonly projectPath: string;
249} {249} {
250 const parts = repository250 const parts: Array<string> = [];
251 .split("/")251 for (const part of repository.split("/")) {
252 .map((part) => part.trim())252 const trimmed = part.trim();
253 .filter((part) => part.length > 0);253 if (trimmed.length > 0) {
254 parts.push(trimmed);
255 }
256 }
254 const projectPath = parts.at(-1) ?? repository.trim();257 const projectPath = parts.at(-1) ?? repository.trim();
255 const namespacePath = parts.length > 1 ? parts.slice(0, -1).join("/") : null;258 const namespacePath = parts.length > 1 ? parts.slice(0, -1).join("/") : null;
256 return { namespacePath, projectPath };259 return { namespacePath, projectPath };
apps/server/src/sourceControl/SourceControlProviderDiscovery.ts+15-6
...@@ -96,15 +96,24 @@ export function unknownAuth(detail?: string): SourceControlProviderAuth {...@@ -96,15 +96,24 @@ export function unknownAuth(detail?: string): SourceControlProviderAuth {
96}96}
9797
98export function combinedAuthOutput(input: SourceControlAuthProbeInput): string {98export function combinedAuthOutput(input: SourceControlAuthProbeInput): string {
99 return [input.stdout, input.stderr].filter((entry) => entry.trim().length > 0).join("\n");99 const parts: string[] = [];
100 for (const entry of [input.stdout, input.stderr]) {
101 if (entry.trim().length > 0) {
102 parts.push(entry);
103 }
104 }
105 return parts.join("\n");
100}106}
101107
102function sanitizedAuthLines(text: string): ReadonlyArray<string> {108function sanitizedAuthLines(text: string): ReadonlyArray<string> {
103 return text109 const lines: string[] = [];
104 .split(/\r?\n/)110 for (const entry of text.split(/\r?\n/)) {
105 .map((entry) => entry.trim())111 const line = entry.trim();
106 .filter((entry) => entry.length > 0)112 if (line.length === 0) continue;
107 .filter((entry) => !/^[-\s]*token(?:\s+scopes?)?:/iu.test(entry));113 if (/^[-\s]*token(?:\s+scopes?)?:/iu.test(line)) continue;
114 lines.push(line);
115 }
116 return lines;
108}117}
109118
110export function firstSafeAuthLine(text: string): string | undefined {119export function firstSafeAuthLine(text: string): string | undefined {
apps/server/src/sourceControl/SourceControlProviderRegistry.ts+11-12
...@@ -92,18 +92,17 @@ function selectProviderContext(...@@ -92,18 +92,17 @@ function selectProviderContext(
92 readonly url: string;92 readonly url: string;
93 }>,93 }>,
94): SourceControlProvider.SourceControlProviderContext | null {94): SourceControlProvider.SourceControlProviderContext | null {
95 const candidates = remotes95 const candidates: Array<SourceControlProvider.SourceControlProviderContext> = [];
96 .map((remote) => {96 for (const remote of remotes) {
97 const provider = detectSourceControlProviderFromRemoteUrl(remote.url);97 const provider = detectSourceControlProviderFromRemoteUrl(remote.url);
98 return provider98 if (provider) {
99 ? {99 candidates.push({
100 provider,100 provider,
101 remoteName: remote.name,101 remoteName: remote.name,
102 remoteUrl: remote.url,102 remoteUrl: remote.url,
103 }103 });
104 : null;104 }
105 })105 }
106 .filter((value): value is SourceControlProvider.SourceControlProviderContext => value !== null);
107106
108 return (107 return (
109 candidates.find((candidate) => candidate.remoteName === "origin") ??108 candidates.find((candidate) => candidate.remoteName === "origin") ??
apps/server/src/vcs/GitVcsDriverCore.ts+62-58
...@@ -1,3 +1,4 @@...@@ -1,3 +1,4 @@
1import * as Arr from "effect/Array";
1import * as Cache from "effect/Cache";2import * as Cache from "effect/Cache";
2import * as Data from "effect/Data";3import * as Data from "effect/Data";
3import * as DateTime from "effect/DateTime";4import * as DateTime from "effect/DateTime";
...@@ -1793,61 +1794,60 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -1793,61 +1794,60 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
1793 }1794 }
1794 }1795 }
17951796
1796 const localBranches = localBranchResult.stdout1797 const localBranches = Arr.filterMap(localBranchResult.stdout.split("\n"), (line) => {
1797 .split("\n")1798 const refName = parseBranchLine(line);
1798 .map(parseBranchLine)1799 return refName === null
1799 .filter((refName): refName is { name: string; current: boolean } => refName !== null)1800 ? Result.failVoid
1800 .map((refName) => ({1801 : Result.succeed({
1801 name: refName.name,1802 name: refName.name,
1802 current: refName.current,1803 current: refName.current,
1803 isRemote: false,1804 isRemote: false,
1804 isDefault: refName.name === defaultBranch,1805 isDefault: refName.name === defaultBranch,
1805 worktreePath: worktreeMap.get(refName.name) ?? null,1806 worktreePath: worktreeMap.get(refName.name) ?? null,
1806 }))1807 });
1807 .toSorted((a, b) => {1808 }).toSorted((a, b) => {
1808 const aPriority = a.current ? 0 : a.isDefault ? 1 : 2;1809 const aPriority = a.current ? 0 : a.isDefault ? 1 : 2;
1809 const bPriority = b.current ? 0 : b.isDefault ? 1 : 2;1810 const bPriority = b.current ? 0 : b.isDefault ? 1 : 2;
1810 if (aPriority !== bPriority) return aPriority - bPriority;1811 if (aPriority !== bPriority) return aPriority - bPriority;
18111812
1812 const aLastCommit = branchLastCommit.get(a.name) ?? 0;1813 const aLastCommit = branchLastCommit.get(a.name) ?? 0;
1813 const bLastCommit = branchLastCommit.get(b.name) ?? 0;1814 const bLastCommit = branchLastCommit.get(b.name) ?? 0;
1814 if (aLastCommit !== bLastCommit) return bLastCommit - aLastCommit;1815 if (aLastCommit !== bLastCommit) return bLastCommit - aLastCommit;
1815 return a.name.localeCompare(b.name);1816 return a.name.localeCompare(b.name);
1816 });1817 });
18171818
1818 const remoteBranches =1819 const remoteBranches =
1819 remoteBranchResult.exitCode === 01820 remoteBranchResult.exitCode === 0
1820 ? remoteBranchResult.stdout1821 ? Arr.filterMap(remoteBranchResult.stdout.split("\n"), (line) => {
1821 .split("\n")1822 const refName = parseBranchLine(line);
1822 .map(parseBranchLine)1823 if (refName === null) {
1823 .filter((refName): refName is { name: string; current: boolean } => refName !== null)1824 return Result.failVoid;
1824 .map((refName) => {1825 }
1825 const parsedRemoteRef = parseRemoteRefWithRemoteNames(refName.name, remoteNames);1826 const parsedRemoteRef = parseRemoteRefWithRemoteNames(refName.name, remoteNames);
1826 const remoteBranch: {1827 const remoteBranch: {
1827 name: string;1828 name: string;
1828 current: boolean;1829 current: boolean;
1829 isRemote: boolean;1830 isRemote: boolean;
1830 remoteName?: string;1831 remoteName?: string;
1831 isDefault: boolean;1832 isDefault: boolean;
1832 worktreePath: string | null;1833 worktreePath: string | null;
1833 } = {1834 } = {
1834 name: refName.name,1835 name: refName.name,
1835 current: false,1836 current: false,
1836 isRemote: true,1837 isRemote: true,
1837 isDefault: false,1838 isDefault: false,
1838 worktreePath: null,1839 worktreePath: null,
1839 };1840 };
1840 if (parsedRemoteRef) {1841 if (parsedRemoteRef) {
1841 remoteBranch.remoteName = parsedRemoteRef.remoteName;1842 remoteBranch.remoteName = parsedRemoteRef.remoteName;
1842 }1843 }
1843 return remoteBranch;1844 return Result.succeed(remoteBranch);
1844 })1845 }).toSorted((a, b) => {
1845 .toSorted((a, b) => {1846 const aLastCommit = branchLastCommit.get(a.name) ?? 0;
1846 const aLastCommit = branchLastCommit.get(a.name) ?? 0;1847 const bLastCommit = branchLastCommit.get(b.name) ?? 0;
1847 const bLastCommit = branchLastCommit.get(b.name) ?? 0;1848 if (aLastCommit !== bLastCommit) return bLastCommit - aLastCommit;
1848 if (aLastCommit !== bLastCommit) return bLastCommit - aLastCommit;1849 return a.name.localeCompare(b.name);
1849 return a.name.localeCompare(b.name);1850 })
1850 })
1851 : [];1851 : [];
18521852
1853 const refs = paginateBranches({1853 const refs = paginateBranches({
...@@ -2106,12 +2106,16 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*...@@ -2106,12 +2106,16 @@ export const makeGitVcsDriverCore = Effect.fn("makeGitVcsDriverCore")(function*
2106 "--no-column",2106 "--no-column",
2107 "--format=%(refname:short)",2107 "--format=%(refname:short)",
2108 ]).pipe(2108 ]).pipe(
2109 Effect.map((stdout) =>2109 Effect.map((stdout) => {
2110 stdout2110 const branchNames: Array<string> = [];
2111 .split("\n")2111 for (const line of stdout.split("\n")) {
2112 .map((line) => line.trim())2112 const branchName = line.trim();
2113 .filter((line) => line.length > 0),2113 if (branchName.length > 0) {
2114 ),2114 branchNames.push(branchName);
2115 }
2116 }
2117 return branchNames;
2118 }),
2115 );2119 );
21162120
2117 return GitVcsDriver.GitVcsDriver.of({2121 return GitVcsDriver.GitVcsDriver.of({
apps/server/src/workspace/Layers/WorkspaceEntries.ts+21-15
...@@ -221,9 +221,13 @@ export const makeWorkspaceEntries = Effect.gen(function* () {...@@ -221,9 +221,13 @@ export const makeWorkspaceEntries = Effect.gen(function* () {
221 return null;221 return null;
222 }222 }
223223
224 const listedPaths = [...listedFiles.paths]224 const listedPaths: Array<string> = [];
225 .map((entry) => toPosixPath(entry))225 for (const rawEntry of listedFiles.paths) {
226 .filter((entry) => entry.length > 0 && !isPathInIgnoredDirectory(entry));226 const entry = toPosixPath(rawEntry);
227 if (entry.length > 0 && !isPathInIgnoredDirectory(entry)) {
228 listedPaths.push(entry);
229 }
230 }
227 const filePaths = yield* vcs.driver.filterIgnoredPaths(cwd, listedPaths).pipe(231 const filePaths = yield* vcs.driver.filterIgnoredPaths(cwd, listedPaths).pipe(
228 Effect.map((paths) => [...paths]),232 Effect.map((paths) => [...paths]),
229 Effect.catch(() => filterVcsIgnoredPaths(cwd, listedPaths)),233 Effect.catch(() => filterVcsIgnoredPaths(cwd, listedPaths)),
...@@ -456,21 +460,23 @@ export const makeWorkspaceEntries = Effect.gen(function* () {...@@ -456,21 +460,23 @@ export const makeWorkspaceEntries = Effect.gen(function* () {
456460
457 const showHidden = endsWithSeparator || prefix.startsWith(".");461 const showHidden = endsWithSeparator || prefix.startsWith(".");
458 const lowerPrefix = prefix.toLowerCase();462 const lowerPrefix = prefix.toLowerCase();
463 const entries: Array<{ readonly name: string; readonly fullPath: string }> = [];
464 for (const dirent of dirents) {
465 if (
466 dirent.isDirectory() &&
467 dirent.name.toLowerCase().startsWith(lowerPrefix) &&
468 (showHidden || !dirent.name.startsWith("."))
469 ) {
470 entries.push({
471 name: dirent.name,
472 fullPath: path.join(parentPath, dirent.name),
473 });
474 }
475 }
459476
460 return {477 return {
461 parentPath,478 parentPath,
462 entries: dirents479 entries: entries.toSorted((left, right) => left.name.localeCompare(right.name)),
463 .filter(
464 (dirent) =>
465 dirent.isDirectory() &&
466 dirent.name.toLowerCase().startsWith(lowerPrefix) &&
467 (showHidden || !dirent.name.startsWith(".")),
468 )
469 .map((dirent) => ({
470 name: dirent.name,
471 fullPath: path.join(parentPath, dirent.name),
472 }))
473 .toSorted((left, right) => left.name.localeCompare(right.name)),
474 };480 };
475 },481 },
476 );482 );
apps/web/src/components/CommandPalette.logic.ts+14-17
...@@ -1,5 +1,7 @@...@@ -1,5 +1,7 @@
1import { type KeybindingCommand, type FilesystemBrowseEntry } from "@t3tools/contracts";1import { type KeybindingCommand, type FilesystemBrowseEntry } from "@t3tools/contracts";
2import type { SidebarThreadSortOrder } from "@t3tools/contracts/settings";2import type { SidebarThreadSortOrder } from "@t3tools/contracts/settings";
3import * as Arr from "effect/Array";
4import * as Result from "effect/Result";
3import { type ReactNode } from "react";5import { type ReactNode } from "react";
4import { sortThreads } from "../lib/threadSort";6import { sortThreads } from "../lib/threadSort";
5import { formatRelativeTimeLabel } from "../timestampFormat";7import { formatRelativeTimeLabel } from "../timestampFormat";
...@@ -252,23 +254,18 @@ export function filterCommandPaletteGroups(input: {...@@ -252,23 +254,18 @@ export function filterCommandPaletteGroups(input: {
252 }254 }
253255
254 return searchableGroups.flatMap((group) => {256 return searchableGroups.flatMap((group) => {
255 const items = group.items257 const items = Arr.filterMap(group.items, (item, index) => {
256 .map((item, index) => {258 const haystack = normalizeSearchText(item.searchTerms.join(" "));
257 const haystack = normalizeSearchText(item.searchTerms.join(" "));259 if (!haystack.includes(normalizedQuery)) {
258 if (!haystack.includes(normalizedQuery)) {260 return Result.failVoid;
259 return null;261 }
260 }262
261263 return Result.succeed({
262 return {264 item,
263 item,265 index,
264 index,266 rank: rankCommandPaletteItemMatch(item, normalizedQuery),
265 rank: rankCommandPaletteItemMatch(item, normalizedQuery),267 });
266 };268 })
267 })
268 .filter(
269 (entry): entry is { item: (typeof group.items)[number]; index: number; rank: number } =>
270 entry !== null,
271 )
272 .toSorted((left, right) => right.rank - left.rank || left.index - right.index)269 .toSorted((left, right) => right.rank - left.rank || left.index - right.index)
273 .map((entry) => entry.item);270 .map((entry) => entry.item);
274271
apps/web/src/components/DiffPanel.tsx+8-6
...@@ -261,12 +261,14 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {...@@ -261,12 +261,14 @@ export default function DiffPanel({ mode = "inline" }: DiffPanelProps) {
261 [selectedCheckpointTurnCount],261 [selectedCheckpointTurnCount],
262 );262 );
263 const conversationCheckpointTurnCount = useMemo(() => {263 const conversationCheckpointTurnCount = useMemo(() => {
264 const turnCounts = orderedTurnDiffSummaries264 const turnCounts: Array<number> = [];
265 .map(265 for (const summary of orderedTurnDiffSummaries) {
266 (summary) =>266 const value =
267 summary.checkpointTurnCount ?? inferredCheckpointTurnCountByTurnId[summary.turnId],267 summary.checkpointTurnCount ?? inferredCheckpointTurnCountByTurnId[summary.turnId];
268 )268 if (typeof value === "number") {
269 .filter((value): value is number => typeof value === "number");269 turnCounts.push(value);
270 }
271 }
270 if (turnCounts.length === 0) {272 if (turnCounts.length === 0) {
271 return undefined;273 return undefined;
272 }274 }
apps/web/src/components/chat/ChatComposer.tsx+6-3
...@@ -1323,9 +1323,12 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)...@@ -1323,9 +1323,12 @@ export const ChatComposer = memo(function ChatComposer(props: ChatComposerProps)
1323 } catch {1323 } catch {
1324 const currentImageIds = new Set(composerImages.map((image) => image.id));1324 const currentImageIds = new Set(composerImages.map((image) => image.id));
1325 const fallbackPersistedAttachments = getPersistedAttachmentsForThread();1325 const fallbackPersistedAttachments = getPersistedAttachmentsForThread();
1326 const fallbackPersistedIds = fallbackPersistedAttachments1326 const fallbackPersistedIds: Array<string> = [];
1327 .map((attachment) => attachment.id)1327 for (const attachment of fallbackPersistedAttachments) {
1328 .filter((id) => currentImageIds.has(id));1328 if (currentImageIds.has(attachment.id)) {
1329 fallbackPersistedIds.push(attachment.id);
1330 }
1331 }
1329 const fallbackPersistedIdSet = new Set(fallbackPersistedIds);1332 const fallbackPersistedIdSet = new Set(fallbackPersistedIds);
1330 const fallbackAttachments = fallbackPersistedAttachments.filter((attachment) =>1333 const fallbackAttachments = fallbackPersistedAttachments.filter((attachment) =>
1331 fallbackPersistedIdSet.has(attachment.id),1334 fallbackPersistedIdSet.has(attachment.id),
apps/web/src/components/chat/ModelPickerContent.tsx+7-2
...@@ -272,8 +272,13 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {...@@ -272,8 +272,13 @@ export const ModelPickerContent = memo(function ModelPickerContent(props: {
272 // ignoring sidebar selection so account-scoped searches can find a272 // ignoring sidebar selection so account-scoped searches can find a
273 // model before the user chooses a specific instance rail item.273 // model before the user chooses a specific instance rail item.
274 if (props.lockedProvider !== null) {274 if (props.lockedProvider !== null) {
275 return rankedMatches275 const lockedProviderMatches: Array<(typeof rankedMatches)[number]> = [];
276 .filter((rankedModel) => matchesLockedProvider(rankedModel.model))276 for (const rankedModel of rankedMatches) {
277 if (matchesLockedProvider(rankedModel.model)) {
278 lockedProviderMatches.push(rankedModel);
279 }
280 }
281 return lockedProviderMatches
277 .toSorted((a, b) => {282 .toSorted((a, b) => {
278 const scoreDelta = a.score - b.score;283 const scoreDelta = a.score - b.score;
279 if (scoreDelta !== 0) {284 if (scoreDelta !== 0) {
apps/web/src/components/chat/TraitsPicker.tsx+17-16
...@@ -381,22 +381,23 @@ export const TraitsPicker = memo(function TraitsPicker({...@@ -381,22 +381,23 @@ export const TraitsPicker = memo(function TraitsPicker({
381 return null;381 return null;
382 }382 }
383383
384 const triggerLabel =384 const triggerLabels: Array<string> = [];
385 descriptors385 for (const descriptor of descriptors) {
386 .map((descriptor) => {386 const label =
387 if (ultrathinkPromptControlled && descriptor.id === primarySelectDescriptor?.id) {387 ultrathinkPromptControlled && descriptor.id === primarySelectDescriptor?.id
388 return "Ultrathink";388 ? "Ultrathink"
389 }389 : descriptor.type === "boolean"
390 if (descriptor.type === "boolean") {390 ? descriptor.id === "fastMode"
391 if (descriptor.id === "fastMode") {391 ? descriptor.currentValue === true
392 return descriptor.currentValue === true ? "Fast" : "Normal";392 ? "Fast"
393 }393 : "Normal"
394 return `${descriptor.label} ${descriptor.currentValue === true ? "On" : "Off"}`;394 : `${descriptor.label} ${descriptor.currentValue === true ? "On" : "Off"}`
395 }395 : getProviderOptionCurrentLabel(descriptor);
396 return getProviderOptionCurrentLabel(descriptor);396 if (typeof label === "string" && label.length > 0) {
397 })397 triggerLabels.push(label);
398 .filter((label): label is string => typeof label === "string" && label.length > 0)398 }
399 .join(" · ") || "";399 }
400 const triggerLabel = triggerLabels.join(" · ");
400401
401 const isCodexStyle = provider === "codex";402 const isCodexStyle = provider === "codex";
402403
apps/web/src/components/chat/modelPickerSearch.ts+7-3
...@@ -68,9 +68,13 @@ export function scoreModelPickerSearch(...@@ -68,9 +68,13 @@ export function scoreModelPickerSearch(
68 let score = 0;68 let score = 0;
6969
70 for (const token of tokens) {70 for (const token of tokens) {
71 const tokenScores = fields71 const tokenScores: Array<number> = [];
72 .map((field, index) => scoreModelPickerSearchToken(field, token, index * 10))72 for (let index = 0; index < fields.length; index += 1) {
73 .filter((fieldScore): fieldScore is number => fieldScore !== null);73 const fieldScore = scoreModelPickerSearchToken(fields[index]!, token, index * 10);
74 if (fieldScore !== null) {
75 tokenScores.push(fieldScore);
76 }
77 }
7478
75 if (tokenScores.length === 0) {79 if (tokenScores.length === 0) {
76 return null;80 return null;
apps/web/src/components/chat/userMessageTerminalContexts.ts+8-5
...@@ -7,11 +7,14 @@ export function buildInlineTerminalContextText(...@@ -7,11 +7,14 @@ export function buildInlineTerminalContextText(
7 header: string;7 header: string;
8 }>,8 }>,
9): string {9): string {
10 return contexts10 const labels: Array<string> = [];
11 .map((context) => context.header.trim())11 for (const context of contexts) {
12 .filter((header) => header.length > 0)12 const header = context.header.trim();
13 .map(formatInlineTerminalContextLabel)13 if (header.length > 0) {
14 .join(" ");14 labels.push(formatInlineTerminalContextLabel(header));
15 }
16 }
17 return labels.join(" ");
15}18}
1619
17export function formatInlineTerminalContextLabel(header: string): string {20export function formatInlineTerminalContextLabel(header: string): string {
apps/web/src/components/settings/ConnectionsSettings.tsx+21-15
...@@ -534,21 +534,27 @@ const PairingLinkListRow = memo(function PairingLinkListRow({...@@ -534,21 +534,27 @@ const PairingLinkListRow = memo(function PairingLinkListRow({
534 const endpoint = selectPairingEndpoint(endpoints, defaultEndpointKey);534 const endpoint = selectPairingEndpoint(endpoints, defaultEndpointKey);
535 return endpoint ? resolveAdvertisedEndpointPairingUrl(endpoint, pairingLink.credential) : null;535 return endpoint ? resolveAdvertisedEndpointPairingUrl(endpoint, pairingLink.credential) : null;
536 }, [defaultEndpointKey, endpoints, pairingLink.credential]);536 }, [defaultEndpointKey, endpoints, pairingLink.credential]);
537 const endpointCopyOptions = useMemo(537 const endpointCopyOptions = useMemo(() => {
538 () =>538 const options: Array<{
539 endpoints539 readonly key: string;
540 .filter((endpoint) => endpoint.status !== "unavailable")540 readonly label: string;
541 .map((endpoint) => {541 readonly url: string;
542 const url = resolveAdvertisedEndpointPairingUrl(endpoint, pairingLink.credential);542 readonly detail: string;
543 return {543 }> = [];
544 key: endpointDefaultPreferenceKey(endpoint),544 for (const endpoint of endpoints) {
545 label: endpoint.label,545 if (endpoint.status === "unavailable") {
546 url,546 continue;
547 detail: isHostedAppPairingUrl(url) ? "Hosted app link" : "Backend pairing URL",547 }
548 };548 const url = resolveAdvertisedEndpointPairingUrl(endpoint, pairingLink.credential);
549 }),549 options.push({
550 [endpoints, pairingLink.credential],550 key: endpointDefaultPreferenceKey(endpoint),
551 );551 label: endpoint.label,
552 url,
553 detail: isHostedAppPairingUrl(url) ? "Hosted app link" : "Backend pairing URL",
554 });
555 }
556 return options;
557 }, [endpoints, pairingLink.credential]);
552 const shareablePairingUrl =558 const shareablePairingUrl =
553 endpointPairingUrl ??559 endpointPairingUrl ??
554 (endpointUrl != null && endpointUrl !== ""560 (endpointUrl != null && endpointUrl !== ""
apps/web/src/components/settings/KeybindingsSettings.logic.ts+17-14
...@@ -140,14 +140,16 @@ export function keybindingConflictLabels(...@@ -140,14 +140,16 @@ export function keybindingConflictLabels(
140 input: { readonly rowId: string; readonly key: string; readonly when: string },140 input: { readonly rowId: string; readonly key: string; readonly when: string },
141): ReadonlyArray<string> {141): ReadonlyArray<string> {
142 if (input.key.trim().length === 0) return [];142 if (input.key.trim().length === 0) return [];
143 const conflicts = rows143 const conflicts: Array<string> = [];
144 .filter(144 for (const candidate of rows) {
145 (candidate) =>145 if (
146 candidate.id !== input.rowId &&146 candidate.id !== input.rowId &&
147 candidate.key === input.key &&147 candidate.key === input.key &&
148 conflictsWithWhen(candidate.when, input.when),148 conflictsWithWhen(candidate.when, input.when)
149 )149 ) {
150 .map((candidate) => commandLabel(candidate.command));150 conflicts.push(commandLabel(candidate.command));
151 }
152 }
151 return [...new Set(conflicts)].toSorted();153 return [...new Set(conflicts)].toSorted();
152}154}
153155
...@@ -274,12 +276,13 @@ export function commandLabel(command: KeybindingCommand): string {...@@ -274,12 +276,13 @@ export function commandLabel(command: KeybindingCommand): string {
274}276}
275277
276function titleCaseCommandSegment(segment: string): string {278function titleCaseCommandSegment(segment: string): string {
277 return segment279 const words: Array<string> = [];
278 .replace(/([a-z0-9])([A-Z])/g, "$1 $2")280 for (const part of segment.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[-_\s]+/)) {
279 .split(/[-_\s]+/)281 if (part.length > 0) {
280 .filter(Boolean)282 words.push(part.slice(0, 1).toUpperCase() + part.slice(1));
281 .map((part) => part.slice(0, 1).toUpperCase() + part.slice(1))283 }
282 .join(" ");284 }
285 return words.join(" ");
283}286}
284287
285export function normalizeShortcutKeyToken(key: string): string | null {288export function normalizeShortcutKeyToken(key: string): string | null {
apps/web/src/components/settings/ProviderInstanceCard.tsx+5-3
...@@ -10,6 +10,8 @@ import {...@@ -10,6 +10,8 @@ import {
10 Trash2Icon,10 Trash2Icon,
11 XIcon,11 XIcon,
12} from "lucide-react";12} from "lucide-react";
13import * as Arr from "effect/Array";
14import * as Result from "effect/Result";
13import { useEffect, useState, type ReactNode } from "react";15import { useEffect, useState, type ReactNode } from "react";
14import {16import {
15 isProviderDriverKind,17 isProviderDriverKind,
...@@ -118,9 +120,9 @@ export function deriveProviderModelsForDisplay(input: {...@@ -118,9 +120,9 @@ export function deriveProviderModelsForDisplay(input: {
118 readonly customModels: ReadonlyArray<string>;120 readonly customModels: ReadonlyArray<string>;
119}): ReadonlyArray<ServerProviderModel> {121}): ReadonlyArray<ServerProviderModel> {
120 const liveCustomModelsBySlug = new Map(122 const liveCustomModelsBySlug = new Map(
121 (input.liveModels ?? [])123 Arr.filterMap(input.liveModels ?? [], (model) =>
122 .filter((model) => model.isCustom)124 model.isCustom ? Result.succeed([model.slug, model] as const) : Result.failVoid,
123 .map((model) => [model.slug, model] as const),125 ),
124 );126 );
125 const serverModels = input.liveModels?.filter((model) => !model.isCustom) ?? [];127 const serverModels = input.liveModels?.filter((model) => !model.isCustom) ?? [];
126 const customModels = input.customModels.map(128 const customModels = input.customModels.map(
apps/web/src/components/settings/SettingsPanels.tsx+31-15
...@@ -14,8 +14,10 @@ import {...@@ -14,8 +14,10 @@ import {
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() {...@@ -1143,7 +1145,12 @@ export function ProviderSettingsPanel() {
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() {...@@ -1249,9 +1256,9 @@ export function ProviderSettingsPanel() {
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 ? (
...@@ -1369,21 +1376,30 @@ export function ArchivedThreadsPanel() {...@@ -1369,21 +1376,30 @@ export function ArchivedThreadsPanel() {
1369 })),1376 })),
1370 );1377 );
13711378
1372 return [...projectsByEnvironmentAndId.values()]1379 const archivedProjects = Array.from(projectsByEnvironmentAndId.values());
1373 .map((project) => ({1380 const groups: Array<{
1374 project,1381 readonly project: (typeof archivedProjects)[number];
1375 threads: threads1382 readonly threads: Array<(typeof threads)[number]>;
1376 .filter(1383 }> = [];
1377 (thread) =>1384 for (const project of archivedProjects) {
1378 thread.projectId === project.id && thread.environmentId === project.environmentId,1385 const projectThreads: Array<(typeof threads)[number]> = [];
1379 )1386 for (const thread of threads) {
1380 .toSorted((left, right) => {1387 if (thread.projectId === project.id && thread.environmentId === project.environmentId) {
1388 projectThreads.push(thread);
1389 }
1390 }
1391 if (projectThreads.length > 0) {
1392 groups.push({
1393 project,
1394 threads: projectThreads.toSorted((left, right) => {
1381 const leftKey = left.archivedAt ?? left.createdAt;1395 const leftKey = left.archivedAt ?? left.createdAt;
1382 const rightKey = right.archivedAt ?? right.createdAt;1396 const rightKey = right.archivedAt ?? right.createdAt;
1383 return rightKey.localeCompare(leftKey) || right.id.localeCompare(left.id);1397 return rightKey.localeCompare(leftKey) || right.id.localeCompare(left.id);
1384 }),1398 }),
1385 }))1399 });
1386 .filter((group) => group.threads.length > 0);1400 }
1401 }
1402 return groups;
1387 }, [archivedSnapshots]);1403 }, [archivedSnapshots]);
13881404
1389 const handleArchivedThreadContextMenu = useCallback(1405 const handleArchivedThreadContextMenu = useCallback(
apps/web/src/composerDraftStore.ts+13-8
...@@ -451,11 +451,13 @@ function cloneModelSelection(selection: ModelSelection): DeepMutable<ModelSelect...@@ -451,11 +451,13 @@ function cloneModelSelection(selection: ModelSelection): DeepMutable<ModelSelect
451function compactModelSelectionByProvider(451function compactModelSelectionByProvider(
452 selections: Partial<Record<ProviderInstanceId, ModelSelection>>,452 selections: Partial<Record<ProviderInstanceId, ModelSelection>>,
453): DeepMutable<Record<ProviderInstanceId, ModelSelection>> {453): DeepMutable<Record<ProviderInstanceId, ModelSelection>> {
454 return Object.fromEntries(454 const entries: Array<[string, DeepMutable<ModelSelection>]> = [];
455 Object.entries(selections)455 for (const [provider, selection] of Object.entries(selections)) {
456 .filter((entry): entry is [string, ModelSelection] => entry[1] !== undefined)456 if (selection !== undefined) {
457 .map(([provider, selection]) => [provider, cloneModelSelection(selection)]),457 entries.push([provider, cloneModelSelection(selection)]);
458 ) as DeepMutable<Record<ProviderInstanceId, ModelSelection>>;458 }
459 }
460 return Object.fromEntries(entries) as DeepMutable<Record<ProviderInstanceId, ModelSelection>>;
459}461}
460462
461const EMPTY_PERSISTED_DRAFT_STORE_STATE = Object.freeze<PersistedComposerDraftStoreState>({463const EMPTY_PERSISTED_DRAFT_STORE_STATE = Object.freeze<PersistedComposerDraftStoreState>({
...@@ -1809,9 +1811,12 @@ function verifyPersistedAttachments(...@@ -1809,9 +1811,12 @@ function verifyPersistedAttachments(
1809 const persistedAttachments = attachments.filter(1811 const persistedAttachments = attachments.filter(
1810 (attachment) => imageIdSet.has(attachment.id) && persistedIdSet.has(attachment.id),1812 (attachment) => imageIdSet.has(attachment.id) && persistedIdSet.has(attachment.id),
1811 );1813 );
1812 const nonPersistedImageIds = current.images1814 const nonPersistedImageIds: string[] = [];
1813 .map((image) => image.id)1815 for (const image of current.images) {
1814 .filter((imageId) => !persistedIdSet.has(imageId));1816 if (!persistedIdSet.has(image.id)) {
1817 nonPersistedImageIds.push(image.id);
1818 }
1819 }
1815 const nextDraft: ComposerThreadDraftState = {1820 const nextDraft: ComposerThreadDraftState = {
1816 ...current,1821 ...current,
1817 persistedAttachments,1822 persistedAttachments,
apps/web/src/environments/runtime/service.ts+6-4
...@@ -1459,10 +1459,12 @@ async function syncSavedEnvironmentConnections(...@@ -1459,10 +1459,12 @@ async function syncSavedEnvironmentConnections(
1459 records: ReadonlyArray<SavedEnvironmentRecord>,1459 records: ReadonlyArray<SavedEnvironmentRecord>,
1460): Promise<void> {1460): Promise<void> {
1461 const expectedEnvironmentIds = new Set(records.map((record) => record.environmentId));1461 const expectedEnvironmentIds = new Set(records.map((record) => record.environmentId));
1462 const staleEnvironmentIds = [...environmentConnections.values()]1462 const staleEnvironmentIds: EnvironmentId[] = [];
1463 .filter((connection) => connection.kind === "saved")1463 for (const connection of environmentConnections.values()) {
1464 .map((connection) => connection.environmentId)1464 if (connection.kind !== "saved") continue;
1465 .filter((environmentId) => !expectedEnvironmentIds.has(environmentId));1465 if (expectedEnvironmentIds.has(connection.environmentId)) continue;
1466 staleEnvironmentIds.push(connection.environmentId);
1467 }
14661468
1467 await Promise.all(1469 await Promise.all(
1468 staleEnvironmentIds.map((environmentId) => disconnectSavedEnvironment(environmentId)),1470 staleEnvironmentIds.map((environmentId) => disconnectSavedEnvironment(environmentId)),
apps/web/src/lib/terminalContext.ts+18-15
...@@ -127,19 +127,18 @@ export function buildTerminalContextPreviewTitle(...@@ -127,19 +127,18 @@ export function buildTerminalContextPreviewTitle(
127 if (contexts.length === 0) {127 if (contexts.length === 0) {
128 return null;128 return null;
129 }129 }
130 const previews = contexts130 const previewParts: string[] = [];
131 .map((context) => {131 for (const context of contexts) {
132 const normalized = normalizeTerminalContextSelection(context);132 const normalized = normalizeTerminalContextSelection(context);
133 if (!normalized) {133 if (!normalized) continue;
134 return null;134 const preview = previewTerminalContextText(normalized.text);
135 }135 previewParts.push(
136 const preview = previewTerminalContextText(normalized.text);136 preview.length > 0
137 return preview.length > 0
138 ? `${formatTerminalContextLabel(normalized)}\n${preview}`137 ? `${formatTerminalContextLabel(normalized)}\n${preview}`
139 : formatTerminalContextLabel(normalized);138 : formatTerminalContextLabel(normalized),
140 })139 );
141 .filter((value): value is string => value !== null)140 }
142 .join("\n\n");141 const previews = previewParts.join("\n\n");
143 return previews.length > 0 ? previews : null;142 return previews.length > 0 ? previews : null;
144}143}
145144
...@@ -152,9 +151,13 @@ function buildTerminalContextBodyLines(selection: TerminalContextSelection): str...@@ -152,9 +151,13 @@ function buildTerminalContextBodyLines(selection: TerminalContextSelection): str
152export function buildTerminalContextBlock(151export function buildTerminalContextBlock(
153 contexts: ReadonlyArray<TerminalContextSelection>,152 contexts: ReadonlyArray<TerminalContextSelection>,
154): string {153): string {
155 const normalizedContexts = contexts154 const normalizedContexts: TerminalContextSelection[] = [];
156 .map((context) => normalizeTerminalContextSelection(context))155 for (const context of contexts) {
157 .filter((context): context is TerminalContextSelection => context !== null);156 const normalized = normalizeTerminalContextSelection(context);
157 if (normalized !== null) {
158 normalizedContexts.push(normalized);
159 }
160 }
158 if (normalizedContexts.length === 0) {161 if (normalizedContexts.length === 0) {
159 return "";162 return "";
160 }163 }
apps/web/src/modelSelection.ts+8-4
...@@ -14,6 +14,8 @@ import {...@@ -14,6 +14,8 @@ import {
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(...@@ -150,9 +152,9 @@ export function getAppModelOptions(
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 );
157159
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(...@@ -198,7 +200,9 @@ export function getAppModelOptionsForInstance(
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 );
203207
204 const customModels = readInstanceCustomModels(settings, entry.instanceId, entry.driverKind);208 const customModels = readInstanceCustomModels(settings, entry.instanceId, entry.driverKind);
apps/web/src/pendingUserInput.ts+8-4
...@@ -33,10 +33,14 @@ function normalizeSelectedOptionLabels(value: string[] | undefined): string[] {...@@ -33,10 +33,14 @@ function normalizeSelectedOptionLabels(value: string[] | undefined): string[] {
33 return [];33 return [];
34 }34 }
3535
36 const normalized = value36 const normalized: string[] = [];
37 .filter((entry): entry is string => typeof entry === "string")37 for (const entry of value) {
38 .map((entry) => entry.trim())38 if (typeof entry !== "string") continue;
39 .filter((entry) => entry.length > 0);39 const trimmed = entry.trim();
40 if (trimmed.length > 0) {
41 normalized.push(trimmed);
42 }
43 }
4044
41 return Array.from(new Set(normalized));45 return Array.from(new Set(normalized));
42}46}
apps/web/src/providerInstances.ts+7-5
...@@ -66,13 +66,15 @@ export interface ProviderInstanceEntry {...@@ -66,13 +66,15 @@ export interface ProviderInstanceEntry {
66 * will take precedence over this fallback.66 * will take precedence over this fallback.
67 */67 */
68function humanizeInstanceId(instanceId: ProviderInstanceId): string {68function humanizeInstanceId(instanceId: ProviderInstanceId): string {
69 return instanceId69 const words: string[] = [];
70 for (const token of instanceId
70 .replace(/[_-]+/g, " ")71 .replace(/[_-]+/g, " ")
71 .replace(/([a-z])([A-Z])/g, "$1 $2")72 .replace(/([a-z])([A-Z])/g, "$1 $2")
72 .split(" ")73 .split(" ")) {
73 .filter((token) => token.length > 0)74 if (token.length === 0) continue;
74 .map((token) => token.charAt(0).toUpperCase() + token.slice(1))75 words.push(token.charAt(0).toUpperCase() + token.slice(1));
75 .join(" ");76 }
77 return words.join(" ");
76}78}
7779
78function driverKindLabel(driverKind: ProviderDriverKind): string {80function driverKindLabel(driverKind: ProviderDriverKind): string {
apps/web/src/providerSkillPresentation.ts+6-5
...@@ -1,11 +1,12 @@...@@ -1,11 +1,12 @@
1import type { ServerProviderSkill } from "@t3tools/contracts";1import type { ServerProviderSkill } from "@t3tools/contracts";
22
3function titleCaseWords(value: string): string {3function titleCaseWords(value: string): string {
4 return value4 const words: string[] = [];
5 .split(/[\s:_-]+/)5 for (const segment of value.split(/[\s:_-]+/)) {
6 .filter((segment) => segment.length > 0)6 if (segment.length === 0) continue;
7 .map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1))7 words.push(segment.charAt(0).toUpperCase() + segment.slice(1));
8 .join(" ");8 }
9 return words.join(" ");
9}10}
1011
11function normalizePathSeparators(pathValue: string): string {12function normalizePathSeparators(pathValue: string): string {
apps/web/src/session-logic.ts+43-37
...@@ -385,28 +385,25 @@ export function deriveActivePlanState(...@@ -385,28 +385,25 @@ export function deriveActivePlanState(
385 if (!Array.isArray(rawPlan)) {385 if (!Array.isArray(rawPlan)) {
386 return null;386 return null;
387 }387 }
388 const steps = rawPlan388 const steps: Array<{
389 .map((entry) => {389 step: string;
390 if (!entry || typeof entry !== "object") return null;390 status: "pending" | "inProgress" | "completed";
391 const record = entry as Record<string, unknown>;391 }> = [];
392 if (typeof record.step !== "string") {392 for (const entry of rawPlan) {
393 return null;393 if (!entry || typeof entry !== "object") {
394 }394 continue;
395 const status =395 }
396 record.status === "completed" || record.status === "inProgress" ? record.status : "pending";396 const record = entry as Record<string, unknown>;
397 return {397 if (typeof record.step !== "string") {
398 step: record.step,398 continue;
399 status,399 }
400 };400 const status =
401 })401 record.status === "completed" || record.status === "inProgress" ? record.status : "pending";
402 .filter(402 steps.push({
403 (403 step: record.step,
404 step,404 status,
405 ): step is {405 });
406 step: string;406 }
407 status: "pending" | "inProgress" | "completed";
408 } => step !== null,
409 );
410 if (steps.length === 0) {407 if (steps.length === 0) {
411 return null;408 return null;
412 }409 }
...@@ -485,14 +482,16 @@ export function deriveWorkLogEntries(...@@ -485,14 +482,16 @@ export function deriveWorkLogEntries(
485 latestTurnId: TurnId | undefined,482 latestTurnId: TurnId | undefined,
486): WorkLogEntry[] {483): WorkLogEntry[] {
487 const ordered = [...activities].toSorted(compareActivitiesByOrder);484 const ordered = [...activities].toSorted(compareActivitiesByOrder);
488 const entries = ordered485 const entries: DerivedWorkLogEntry[] = [];
489 .filter((activity) => (latestTurnId ? activity.turnId === latestTurnId : true))486 for (const activity of ordered) {
490 .filter((activity) => activity.kind !== "tool.started")487 if (latestTurnId && activity.turnId !== latestTurnId) continue;
491 .filter((activity) => activity.kind !== "task.started")488 if (activity.kind === "tool.started") continue;
492 .filter((activity) => activity.kind !== "context-window.updated")489 if (activity.kind === "task.started") continue;
493 .filter((activity) => activity.summary !== "Checkpoint captured")490 if (activity.kind === "context-window.updated") continue;
494 .filter((activity) => !isPlanBoundaryToolActivity(activity))491 if (activity.summary === "Checkpoint captured") continue;
495 .map(toDerivedWorkLogEntry);492 if (isPlanBoundaryToolActivity(activity)) continue;
493 entries.push(toDerivedWorkLogEntry(activity));
494 }
496 return collapseDerivedWorkLogEntries(entries).map(495 return collapseDerivedWorkLogEntries(entries).map(
497 ({ activityKind: _activityKind, collapseKey: _collapseKey, ...entry }) => entry,496 ({ activityKind: _activityKind, collapseKey: _collapseKey, ...entry }) => entry,
498 );497 );
...@@ -834,9 +833,13 @@ function formatCommandValue(value: unknown): string | null {...@@ -834,9 +833,13 @@ function formatCommandValue(value: unknown): string | null {
834 if (!Array.isArray(value)) {833 if (!Array.isArray(value)) {
835 return null;834 return null;
836 }835 }
837 const parts = value836 const parts: Array<string> = [];
838 .map((entry) => asTrimmedString(entry))837 for (const entry of value) {
839 .filter((entry): entry is string => entry !== null);838 const part = asTrimmedString(entry);
839 if (part !== null) {
840 parts.push(part);
841 }
842 }
840 if (parts.length === 0) {843 if (parts.length === 0) {
841 return null;844 return null;
842 }845 }
...@@ -920,10 +923,13 @@ function normalizePreviewForComparison(value: string | null | undefined): string...@@ -920,10 +923,13 @@ function normalizePreviewForComparison(value: string | null | undefined): string
920}923}
921924
922function summarizeToolTextOutput(value: string): string | null {925function summarizeToolTextOutput(value: string): string | null {
923 const lines = value926 const lines: Array<string> = [];
924 .split(/\r?\n/u)927 for (const rawLine of value.split(/\r?\n/u)) {
925 .map((line) => normalizeInlinePreview(line))928 const line = normalizeInlinePreview(rawLine);
926 .filter((line) => line.length > 0);929 if (line.length > 0) {
930 lines.push(line);
931 }
932 }
927 const firstLine = lines.find((line) => line !== "```");933 const firstLine = lines.find((line) => line !== "```");
928 if (firstLine) {934 if (firstLine) {
929 return truncateInlinePreview(firstLine);935 return truncateInlinePreview(firstLine);
packages/shared/src/git.ts+5-1
...@@ -6,6 +6,8 @@ import type {...@@ -6,6 +6,8 @@ import type {
6 VcsStatusResult,6 VcsStatusResult,
7 VcsStatusStreamEvent,7 VcsStatusStreamEvent,
8} from "@t3tools/contracts";8} from "@t3tools/contracts";
9import * as Arr from "effect/Array";
10import * as Result from "effect/Result";
9import { detectSourceControlProviderFromRemoteUrl } from "./sourceControl.ts";11import { detectSourceControlProviderFromRemoteUrl } from "./sourceControl.ts";
1012
11export const WORKTREE_BRANCH_PREFIX = "t3code";13export const WORKTREE_BRANCH_PREFIX = "t3code";
...@@ -172,7 +174,9 @@ export function dedupeRemoteBranchesWithLocalMatches(...@@ -172,7 +174,9 @@ export function dedupeRemoteBranchesWithLocalMatches(
172 refs: ReadonlyArray<VcsRef>,174 refs: ReadonlyArray<VcsRef>,
173): ReadonlyArray<VcsRef> {175): ReadonlyArray<VcsRef> {
174 const localBranchNames = new Set(176 const localBranchNames = new Set(
175 refs.filter((refName) => !refName.isRemote).map((refName) => refName.name),177 Arr.filterMap(refs, (refName) =>
178 refName.isRemote ? Result.failVoid : Result.succeed(refName.name),
179 ),
176 );180 );
177181
178 return refs.filter((refName) => {182 return refs.filter((refName) => {
packages/shared/src/observability.ts+7-5
...@@ -184,11 +184,13 @@ function normalizeJsonValue(value: unknown, seen: WeakSet<object> = new WeakSet(...@@ -184,11 +184,13 @@ function normalizeJsonValue(value: unknown, seen: WeakSet<object> = new WeakSet(
184export function compactTraceAttributes(184export function compactTraceAttributes(
185 attributes: Readonly<Record<string, unknown>>,185 attributes: Readonly<Record<string, unknown>>,
186): TraceAttributes {186): TraceAttributes {
187 return Object.fromEntries(187 const entries: Array<[string, unknown]> = [];
188 Object.entries(attributes)188 for (const [key, value] of Object.entries(attributes)) {
189 .filter(([, value]) => value !== undefined)189 if (value !== undefined) {
190 .map(([key, value]) => [key, normalizeJsonValue(value)]),190 entries.push([key, normalizeJsonValue(value)]);
191 );191 }
192 }
193 return Object.fromEntries(entries);
192}194}
193195
194function formatTraceExit(exit: Exit.Exit<unknown, unknown>): EffectTraceRecord["exit"] {196function formatTraceExit(exit: Exit.Exit<unknown, unknown>): EffectTraceRecord["exit"] {
packages/shared/src/semver.ts+22-9
...@@ -9,10 +9,13 @@ const SEMVER_NUMBER_SEGMENT = /^\d+$/;...@@ -9,10 +9,13 @@ const SEMVER_NUMBER_SEGMENT = /^\d+$/;
99
10export function normalizeSemverVersion(version: string): string {10export function normalizeSemverVersion(version: string): string {
11 const [main, prerelease] = version.trim().split("-", 2);11 const [main, prerelease] = version.trim().split("-", 2);
12 const segments = (main ?? "")12 const segments: string[] = [];
13 .split(".")13 for (const segment of (main ?? "").split(".")) {
14 .map((segment) => segment.trim())14 const trimmed = segment.trim();
15 .filter((segment) => segment.length > 0);15 if (trimmed.length > 0) {
16 segments.push(trimmed);
17 }
18 }
1619
17 if (segments.length === 2) {20 if (segments.length === 2) {
18 segments.push("0");21 segments.push("0");
...@@ -52,14 +55,24 @@ export function parseSemver(value: string): ParsedSemver | null {...@@ -52,14 +55,24 @@ export function parseSemver(value: string): ParsedSemver | null {
52 major,55 major,
53 minor,56 minor,
54 patch,57 patch,
55 prerelease:58 prerelease: parsePrereleaseSegments(prerelease),
56 prerelease
57 ?.split(".")
58 .map((segment) => segment.trim())
59 .filter((segment) => segment.length > 0) ?? [],
60 };59 };
61}60}
6261
62function parsePrereleaseSegments(prerelease: string | undefined): ReadonlyArray<string> {
63 if (prerelease === undefined) {
64 return [];
65 }
66 const segments: string[] = [];
67 for (const segment of prerelease.split(".")) {
68 const trimmed = segment.trim();
69 if (trimmed.length > 0) {
70 segments.push(trimmed);
71 }
72 }
73 return segments;
74}
75
63function comparePrereleaseIdentifier(left: string, right: string): number {76function comparePrereleaseIdentifier(left: string, right: string): number {
64 const leftNumeric = SEMVER_NUMBER_SEGMENT.test(left);77 const leftNumeric = SEMVER_NUMBER_SEGMENT.test(left);
65 const rightNumeric = SEMVER_NUMBER_SEGMENT.test(right);78 const rightNumeric = SEMVER_NUMBER_SEGMENT.test(right);
packages/shared/src/shell.ts+13-9
...@@ -321,11 +321,12 @@ function resolveWindowsPathExtensions(env: NodeJS.ProcessEnv): ReadonlyArray<str...@@ -321,11 +321,12 @@ function resolveWindowsPathExtensions(env: NodeJS.ProcessEnv): ReadonlyArray<str
321 const fallback = [".COM", ".EXE", ".BAT", ".CMD"];321 const fallback = [".COM", ".EXE", ".BAT", ".CMD"];
322 if (!rawValue) return fallback;322 if (!rawValue) return fallback;
323323
324 const parsed = rawValue324 const parsed: string[] = [];
325 .split(";")325 for (const entry of rawValue.split(";")) {
326 .map((entry) => entry.trim())326 const trimmed = entry.trim();
327 .filter((entry) => entry.length > 0)327 if (trimmed.length === 0) continue;
328 .map((entry) => (entry.startsWith(".") ? entry.toUpperCase() : `.${entry.toUpperCase()}`));328 parsed.push(trimmed.startsWith(".") ? trimmed.toUpperCase() : `.${trimmed.toUpperCase()}`);
329 }
329 return parsed.length > 0 ? Array.from(new Set(parsed)) : fallback;330 return parsed.length > 0 ? Array.from(new Set(parsed)) : fallback;
330}331}
331332
...@@ -397,10 +398,13 @@ export function resolveCommandPath(...@@ -397,10 +398,13 @@ export function resolveCommandPath(
397398
398 const pathValue = resolvePathEnvironmentVariable(env);399 const pathValue = resolvePathEnvironmentVariable(env);
399 if (pathValue.length === 0) return null;400 if (pathValue.length === 0) return null;
400 const pathEntries = pathValue401 const pathEntries: string[] = [];
401 .split(pathDelimiterForPlatform(platform))402 for (const entry of pathValue.split(pathDelimiterForPlatform(platform))) {
402 .map((entry) => stripWrappingQuotes(entry.trim()))403 const pathEntry = stripWrappingQuotes(entry.trim());
403 .filter((entry) => entry.length > 0);404 if (pathEntry.length > 0) {
405 pathEntries.push(pathEntry);
406 }
407 }
404408
405 for (const pathEntry of pathEntries) {409 for (const pathEntry of pathEntries) {
406 for (const candidate of commandCandidates) {410 for (const candidate of commandCandidates) {
packages/shared/src/toolActivity.ts+7-3
...@@ -22,9 +22,13 @@ function normalizeCommandValue(value: unknown): string | undefined {...@@ -22,9 +22,13 @@ function normalizeCommandValue(value: unknown): string | undefined {
22 if (!Array.isArray(value)) {22 if (!Array.isArray(value)) {
23 return undefined;23 return undefined;
24 }24 }
25 const parts = value25 const parts: string[] = [];
26 .map((entry) => asTrimmedString(entry))26 for (const entry of value) {
27 .filter((entry): entry is string => entry !== undefined);27 const part = asTrimmedString(entry);
28 if (part !== undefined) {
29 parts.push(part);
30 }
31 }
28 return parts.length > 0 ? parts.join(" ") : undefined;32 return parts.length > 0 ? parts.join(" ") : undefined;
29}33}
3034
packages/ssh/src/config.ts+9-4
...@@ -15,12 +15,17 @@ function stripInlineComment(line: string): string {...@@ -15,12 +15,17 @@ function stripInlineComment(line: string): string {
15}15}
1616
17function splitDirectiveArgs(value: string): ReadonlyArray<string> {17function splitDirectiveArgs(value: string): ReadonlyArray<string> {
18 return value18 const args: Array<string> = [];
19 for (const rawEntry of value
19 .replace(/=(?!=)/gu, " ")20 .replace(/=(?!=)/gu, " ")
20 .trim()21 .trim()
21 .split(/\s+/u)22 .split(/\s+/u)) {
22 .map((entry) => entry.trim())23 const entry = rawEntry.trim();
23 .filter((entry) => entry.length > 0);24 if (entry.length > 0) {
25 args.push(entry);
26 }
27 }
28 return args;
24}29}
2530
26function expandHomePath(input: string, homeDir: string): string {31function expandHomePath(input: string, homeDir: string): string {
packages/tailscale/src/tailscale.ts+8-5
...@@ -112,11 +112,14 @@ export const parseTailscaleStatus = (...@@ -112,11 +112,14 @@ export const parseTailscaleStatus = (
112 Effect.mapError((cause) => new TailscaleStatusParseError({ cause })),112 Effect.mapError((cause) => new TailscaleStatusParseError({ cause })),
113 Effect.map((parsed) => {113 Effect.map((parsed) => {
114 const rawIps = parsed.Self?.TailscaleIPs;114 const rawIps = parsed.Self?.TailscaleIPs;
115 const tailnetIpv4Addresses = Array.isArray(rawIps)115 const tailnetIpv4Addresses: Array<string> = [];
116 ? rawIps116 if (Array.isArray(rawIps)) {
117 .filter((address): address is string => typeof address === "string")117 for (const address of rawIps) {
118 .filter(isTailscaleIpv4Address)118 if (typeof address === "string" && isTailscaleIpv4Address(address)) {
119 : [];119 tailnetIpv4Addresses.push(address);
120 }
121 }
122 }
120123
121 return {124 return {
122 magicDnsName: normalizeMagicDnsName(parsed),125 magicDnsName: normalizeMagicDnsName(parsed),