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