| author | |
| committer | |
| log | 31268945f88a712fad9309d5ea9290cb8496af83 |
| tree | f10d21c963f7de25996020995052981f7b0f1aee |
| parent | 6b3050ee740f510ad06c12a91ecd7e338792dce0 |
| signature | Signed by PGP key B5690EEEBB952194 |
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* () { |
| 64 | 64 | ||
| 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 | pairingLinks | 68 | 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.label | 77 | 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 | ); |
| 97 | 103 |
apps/server/src/git/remoteRefs.ts+8-4| ... | @@ -1,8 +1,12 @@ | ... | @@ -1,8 +1,12 @@ |
| 1 | export function parseRemoteNamesInGitOrder(stdout: string): ReadonlyArray<string> { | 1 | export function parseRemoteNamesInGitOrder(stdout: string): ReadonlyArray<string> { |
| 2 | return stdout | 2 | 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 | } |
| 7 | 11 | ||
| 8 | export function parseRemoteNames(stdout: string): ReadonlyArray<string> { | 12 | export 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; |
| 37 | 37 | ||
| 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"; |
| 32 | import * as Option from "effect/Option"; | 32 | import * as Option from "effect/Option"; |
| 33 | import * as Predicate from "effect/Predicate"; | 33 | import * as Predicate from "effect/Predicate"; |
| 34 | import * as PubSub from "effect/PubSub"; | 34 | import * as PubSub from "effect/PubSub"; |
| 35 | import * as Result from "effect/Result"; | ||
| 35 | import * as Schema from "effect/Schema"; | 36 | import * as Schema from "effect/Schema"; |
| 36 | import * as SchemaIssue from "effect/SchemaIssue"; | 37 | import * as SchemaIssue from "effect/SchemaIssue"; |
| 37 | import * as SchemaTransformation from "effect/SchemaTransformation"; | 38 | import * 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 | } |
| 540 | 541 | ||
| 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 @@ |
| 1 | import { | 1 | import { |
| 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 | } |
| 703 | 704 | ||
| 704 | const staleCheckpointRefs = thread.checkpoints | 705 | 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 | } | ||
| 707 | 711 | ||
| 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 | ]); |
| 539 | 539 | ||
| 540 | const latestUserMessageAt = | 540 | let latestUserMessageAt: string | null = null; |
| 541 | messages | 541 | 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 | } | ||
| 546 | 549 | ||
| 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"; |
| 27 | import * as Arr from "effect/Array"; | ||
| 27 | import * as Effect from "effect/Effect"; | 28 | import * as Effect from "effect/Effect"; |
| 28 | import * as Layer from "effect/Layer"; | 29 | import * as Layer from "effect/Layer"; |
| 29 | import * as Option from "effect/Option"; | 30 | import * as Option from "effect/Option"; |
| 31 | import * as Result from "effect/Result"; | ||
| 30 | import * as Schema from "effect/Schema"; | 32 | import * as Schema from "effect/Schema"; |
| 31 | import * as Struct from "effect/Struct"; | 33 | import * as Struct from "effect/Struct"; |
| 32 | import * as SqlClient from "effect/unstable/sql/SqlClient"; | 34 | import * 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* () { |
| 1488 | 1490 | ||
| 1489 | const snapshot = { | 1491 | const snapshot = { |
| 1490 | snapshotSequence: computeSnapshotSequence(stateRows), | 1492 | snapshotSequence: computeSnapshotSequence(stateRows), |
| 1491 | projects: projectRows | 1493 | 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: threadRows | 1498 | : 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 | }; |
| 1521 | 1525 | ||
| ... | @@ -1621,11 +1625,13 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { | ... | @@ -1621,11 +1625,13 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () { |
| 1621 | 1625 | ||
| 1622 | const snapshot = { | 1626 | const snapshot = { |
| 1623 | snapshotSequence: computeSnapshotSequence(stateRows), | 1627 | snapshotSequence: computeSnapshotSequence(stateRows), |
| 1624 | projects: projectRows | 1628 | 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( |
| 245 | function normalizeClaudeStreamMessages( | 245 | function 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 | } |
| 318 | 318 | ||
| 319 | function toTitleCaseWords(value: string): string { | 319 | function toTitleCaseWords(value: string): string { |
| 320 | return value | 320 | 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 | } |
| 326 | 328 | ||
| 327 | function claudeSubscriptionLabel(subscriptionType: string | undefined): string | undefined { | 329 | function 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 | }); |
| 321 | 321 | ||
| 322 | const emptyCodexModelsFromSettings = (codexSettings: CodexSettings): ServerProvider["models"] => | 322 | const emptyCodexModelsFromSettings = (codexSettings: CodexSettings): ServerProvider["models"] => { |
| 323 | codexSettings.customModels | 323 | 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 | }; | ||
| 332 | 337 | ||
| 333 | const makePendingCodexProvider = ( | 338 | const 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 | } |
| 598 | 598 | ||
| 599 | const targetModelSlugs = new Set( | 599 | const targetModelSlugs = new Set<string>(); |
| 600 | existingModels | 600 | 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 | } |
| 722 | 723 | ||
| 723 | function joinProviderMessages(...messages: ReadonlyArray<string | undefined>): string | undefined { | 724 | function joinProviderMessages(...messages: ReadonlyArray<string | undefined>): string | undefined { |
| 724 | const parts = messages | 725 | 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 | } |
| 729 | 734 | ||
| ... | @@ -790,11 +795,13 @@ export function parseCursorCliConfigChannel(raw: string): string | undefined { | ... | @@ -790,11 +795,13 @@ export function parseCursorCliConfigChannel(raw: string): string | undefined { |
| 790 | } | 795 | } |
| 791 | 796 | ||
| 792 | function toTitleCaseWords(value: string): string { | 797 | function toTitleCaseWords(value: string): string { |
| 793 | return value | 798 | 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 | } |
| 799 | 806 | ||
| 800 | function cursorSubscriptionLabel(subscriptionType: string | undefined): string | undefined { | 807 | function 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)); |
| 1368 | 1368 | ||
| 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 | } | ||
| 1375 | 1378 | ||
| 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 | } |
| 138 | 138 | ||
| 139 | function titleCaseSlug(value: string): string { | 139 | function titleCaseSlug(value: string): string { |
| 140 | return value | 140 | 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 | } |
| 146 | 148 | ||
| 147 | function inferDefaultVariant( | 149 | function 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.availableModes | 129 | 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 !== undefined | 137 | 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 = value | 190 | 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 | } |
| 194 | 201 | ||
| ... | @@ -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 = content | 232 | 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 | } |
| 239 | 248 |
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 | providers | 218 | 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 === 0 | 227 | 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 = repository | 211 | 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 = repository | 250 | 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 | } |
| 97 | 97 | ||
| 98 | export function combinedAuthOutput(input: SourceControlAuthProbeInput): string { | 98 | export 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 | } |
| 101 | 107 | ||
| 102 | function sanitizedAuthLines(text: string): ReadonlyArray<string> { | 108 | function sanitizedAuthLines(text: string): ReadonlyArray<string> { |
| 103 | return text | 109 | 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 | } |
| 109 | 118 | ||
| 110 | export function firstSafeAuthLine(text: string): string | undefined { | 119 | export 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 = remotes | 95 | 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 provider | 98 | 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); | ||
| 107 | 106 | ||
| 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 @@ |
| 1 | import * as Arr from "effect/Array"; | ||
| 1 | import * as Cache from "effect/Cache"; | 2 | import * as Cache from "effect/Cache"; |
| 2 | import * as Data from "effect/Data"; | 3 | import * as Data from "effect/Data"; |
| 3 | import * as DateTime from "effect/DateTime"; | 4 | import * 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 | } |
| 1795 | 1796 | ||
| 1796 | const localBranches = localBranchResult.stdout | 1797 | 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; |
| 1811 | 1812 | ||
| 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 | }); |
| 1817 | 1818 | ||
| 1818 | const remoteBranches = | 1819 | const remoteBranches = |
| 1819 | remoteBranchResult.exitCode === 0 | 1820 | remoteBranchResult.exitCode === 0 |
| 1820 | ? remoteBranchResult.stdout | 1821 | ? 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 | : []; |
| 1852 | 1852 | ||
| 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 | stdout | 2110 | 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 | ); |
| 2116 | 2120 | ||
| 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 | } |
| 223 | 223 | ||
| 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* () { |
| 456 | 460 | ||
| 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 | } | ||
| 459 | 476 | ||
| 460 | return { | 477 | return { |
| 461 | parentPath, | 478 | parentPath, |
| 462 | entries: dirents | 479 | 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 @@ |
| 1 | import { type KeybindingCommand, type FilesystemBrowseEntry } from "@t3tools/contracts"; | 1 | import { type KeybindingCommand, type FilesystemBrowseEntry } from "@t3tools/contracts"; |
| 2 | import type { SidebarThreadSortOrder } from "@t3tools/contracts/settings"; | 2 | import type { SidebarThreadSortOrder } from "@t3tools/contracts/settings"; |
| 3 | import * as Arr from "effect/Array"; | ||
| 4 | import * as Result from "effect/Result"; | ||
| 3 | import { type ReactNode } from "react"; | 5 | import { type ReactNode } from "react"; |
| 4 | import { sortThreads } from "../lib/threadSort"; | 6 | import { sortThreads } from "../lib/threadSort"; |
| 5 | import { formatRelativeTimeLabel } from "../timestampFormat"; | 7 | import { formatRelativeTimeLabel } from "../timestampFormat"; |
| ... | @@ -252,23 +254,18 @@ export function filterCommandPaletteGroups(input: { | ... | @@ -252,23 +254,18 @@ export function filterCommandPaletteGroups(input: { |
| 252 | } | 254 | } |
| 253 | 255 | ||
| 254 | return searchableGroups.flatMap((group) => { | 256 | return searchableGroups.flatMap((group) => { |
| 255 | const items = group.items | 257 | 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 | |
| 261 | 263 | 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); |
| 274 | 271 |
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 = orderedTurnDiffSummaries | 264 | 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 = fallbackPersistedAttachments | 1326 | 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 a | 272 | // 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 rankedMatches | 275 | 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 | } |
| 383 | 383 | ||
| 384 | const triggerLabel = | 384 | const triggerLabels: Array<string> = []; |
| 385 | descriptors | 385 | 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(" · "); | ||
| 400 | 401 | ||
| 401 | const isCodexStyle = provider === "codex"; | 402 | const isCodexStyle = provider === "codex"; |
| 402 | 403 |
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; |
| 69 | 69 | ||
| 70 | for (const token of tokens) { | 70 | for (const token of tokens) { |
| 71 | const tokenScores = fields | 71 | 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 | } | ||
| 74 | 78 | ||
| 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 contexts | 10 | 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 | } |
| 16 | 19 | ||
| 17 | export function formatInlineTerminalContextLabel(header: string): string { | 20 | export 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 | endpoints | 539 | 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 = rows | 143 | 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 | } |
| 153 | 155 | ||
| ... | @@ -274,12 +276,13 @@ export function commandLabel(command: KeybindingCommand): string { | ... | @@ -274,12 +276,13 @@ export function commandLabel(command: KeybindingCommand): string { |
| 274 | } | 276 | } |
| 275 | 277 | ||
| 276 | function titleCaseCommandSegment(segment: string): string { | 278 | function titleCaseCommandSegment(segment: string): string { |
| 277 | return segment | 279 | 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 | } |
| 284 | 287 | ||
| 285 | export function normalizeShortcutKeyToken(key: string): string | null { | 288 | export 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"; |
| 13 | import * as Arr from "effect/Array"; | ||
| 14 | import * as Result from "effect/Result"; | ||
| 13 | import { useEffect, useState, type ReactNode } from "react"; | 15 | import { useEffect, useState, type ReactNode } from "react"; |
| 14 | import { | 16 | import { |
| 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 { |
| 14 | import { scopeThreadRef } from "@t3tools/client-runtime"; | 14 | import { scopeThreadRef } from "@t3tools/client-runtime"; |
| 15 | import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings"; | 15 | import { DEFAULT_UNIFIED_SETTINGS } from "@t3tools/contracts/settings"; |
| 16 | import { createModelSelection } from "@t3tools/shared/model"; | 16 | import { createModelSelection } from "@t3tools/shared/model"; |
| 17 | import * as Arr from "effect/Array"; | ||
| 17 | import * as Duration from "effect/Duration"; | 18 | import * as Duration from "effect/Duration"; |
| 18 | import * as Equal from "effect/Equal"; | 19 | import * as Equal from "effect/Equal"; |
| 20 | import * as Result from "effect/Result"; | ||
| 19 | import { APP_VERSION, HOSTED_APP_CHANNEL, HOSTED_APP_CHANNEL_LABEL } from "../../branding"; | 21 | import { APP_VERSION, HOSTED_APP_CHANNEL, HOSTED_APP_CHANNEL_LABEL } from "../../branding"; |
| 20 | import { | 22 | import { |
| 21 | canCheckForUpdate, | 23 | canCheckForUpdate, |
| ... | @@ -1143,7 +1145,12 @@ export function ProviderSettingsPanel() { | ... | @@ -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 | ); |
| 1371 | 1378 | ||
| 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: threads | 1382 | 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]); |
| 1388 | 1404 | ||
| 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 |
| 451 | function compactModelSelectionByProvider( | 451 | function 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 | } |
| 460 | 462 | ||
| 461 | const EMPTY_PERSISTED_DRAFT_STORE_STATE = Object.freeze<PersistedComposerDraftStoreState>({ | 463 | const 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.images | 1814 | 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 | } | ||
| 1466 | 1468 | ||
| 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 = contexts | 130 | 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 | } |
| 145 | 144 | ||
| ... | @@ -152,9 +151,13 @@ function buildTerminalContextBodyLines(selection: TerminalContextSelection): str | ... | @@ -152,9 +151,13 @@ function buildTerminalContextBodyLines(selection: TerminalContextSelection): str |
| 152 | export function buildTerminalContextBlock( | 151 | export function buildTerminalContextBlock( |
| 153 | contexts: ReadonlyArray<TerminalContextSelection>, | 152 | contexts: ReadonlyArray<TerminalContextSelection>, |
| 154 | ): string { | 153 | ): string { |
| 155 | const normalizedContexts = contexts | 154 | 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"; |
| 15 | import { getComposerProviderState } from "./components/chat/composerProviderState"; | 15 | import { getComposerProviderState } from "./components/chat/composerProviderState"; |
| 16 | import { UnifiedSettings } from "@t3tools/contracts/settings"; | 16 | import { UnifiedSettings } from "@t3tools/contracts/settings"; |
| 17 | import * as Arr from "effect/Array"; | ||
| 18 | import * as Result from "effect/Result"; | ||
| 17 | import { | 19 | import { |
| 18 | getDefaultServerModel, | 20 | getDefaultServerModel, |
| 19 | getProviderModels, | 21 | getProviderModels, |
| ... | @@ -150,9 +152,9 @@ export function getAppModelOptions( | ... | @@ -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 | ); |
| 157 | 159 | ||
| 158 | // Read from the default instance's config first (that's where edits | 160 | // Read from the default instance's config first (that's where edits |
| ... | @@ -198,7 +200,9 @@ export function getAppModelOptionsForInstance( | ... | @@ -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 | ); |
| 203 | 207 | ||
| 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 | } |
| 35 | 35 | ||
| 36 | const normalized = value | 36 | 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 | } | ||
| 40 | 44 | ||
| 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 | */ |
| 68 | function humanizeInstanceId(instanceId: ProviderInstanceId): string { | 68 | function humanizeInstanceId(instanceId: ProviderInstanceId): string { |
| 69 | return instanceId | 69 | 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 | } |
| 77 | 79 | ||
| 78 | function driverKindLabel(driverKind: ProviderDriverKind): string { | 80 | function driverKindLabel(driverKind: ProviderDriverKind): string { |
apps/web/src/providerSkillPresentation.ts+6-5| ... | @@ -1,11 +1,12 @@ | ... | @@ -1,11 +1,12 @@ |
| 1 | import type { ServerProviderSkill } from "@t3tools/contracts"; | 1 | import type { ServerProviderSkill } from "@t3tools/contracts"; |
| 2 | 2 | ||
| 3 | function titleCaseWords(value: string): string { | 3 | function titleCaseWords(value: string): string { |
| 4 | return value | 4 | 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 | } |
| 10 | 11 | ||
| 11 | function normalizePathSeparators(pathValue: string): string { | 12 | function 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 = rawPlan | 388 | 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 = ordered | 485 | 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 = value | 836 | 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 | } |
| 921 | 924 | ||
| 922 | function summarizeToolTextOutput(value: string): string | null { | 925 | function summarizeToolTextOutput(value: string): string | null { |
| 923 | const lines = value | 926 | 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"; |
| 9 | import * as Arr from "effect/Array"; | ||
| 10 | import * as Result from "effect/Result"; | ||
| 9 | import { detectSourceControlProviderFromRemoteUrl } from "./sourceControl.ts"; | 11 | import { detectSourceControlProviderFromRemoteUrl } from "./sourceControl.ts"; |
| 10 | 12 | ||
| 11 | export const WORKTREE_BRANCH_PREFIX = "t3code"; | 13 | export 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 | ); |
| 177 | 181 | ||
| 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( |
| 184 | export function compactTraceAttributes( | 184 | export 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 | } |
| 193 | 195 | ||
| 194 | function formatTraceExit(exit: Exit.Exit<unknown, unknown>): EffectTraceRecord["exit"] { | 196 | function 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+$/; |
| 9 | 9 | ||
| 10 | export function normalizeSemverVersion(version: string): string { | 10 | export 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 | } | ||
| 16 | 19 | ||
| 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 | } |
| 62 | 61 | ||
| 62 | function 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 | |||
| 63 | function comparePrereleaseIdentifier(left: string, right: string): number { | 76 | function 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; |
| 323 | 323 | ||
| 324 | const parsed = rawValue | 324 | 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 | } |
| 331 | 332 | ||
| ... | @@ -397,10 +398,13 @@ export function resolveCommandPath( | ... | @@ -397,10 +398,13 @@ export function resolveCommandPath( |
| 397 | 398 | ||
| 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 = pathValue | 401 | 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 | } | ||
| 404 | 408 | ||
| 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 = value | 25 | 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 | } |
| 30 | 34 |
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 | } |
| 16 | 16 | ||
| 17 | function splitDirectiveArgs(value: string): ReadonlyArray<string> { | 17 | function splitDirectiveArgs(value: string): ReadonlyArray<string> { |
| 18 | return value | 18 | 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 | } |
| 25 | 30 | ||
| 26 | function expandHomePath(input: string, homeDir: string): string { | 31 | function 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 | ? rawIps | 116 | 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 | } | ||
| 120 | 123 | ||
| 121 | return { | 124 | return { |
| 122 | magicDnsName: normalizeMagicDnsName(parsed), | 125 | magicDnsName: normalizeMagicDnsName(parsed), |