Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .macroscope/check-run-agents/effect-service-conventions.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Effect Service Conventions
model: gpt-5-6-sol
effort: medium
model: gpt-6-sol
effort: max
input: incremental
tools:
- browse_code
Expand Down
4 changes: 2 additions & 2 deletions .macroscope/check-run-agents/ui-consistency.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: UI Consistency
model: gpt-5-6-sol
effort: medium
model: gpt-6-sol
effort: max
input: incremental
tools:
- browse_code
Expand Down
14 changes: 12 additions & 2 deletions apps/desktop/src/app/DesktopObservability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -356,7 +356,13 @@ const readPersistedObservabilitySettings: Effect.Effect<
const resolveOtlpEndpoints = Effect.gen(function* () {
const otel = yield* OtelEnvironment.load;
if (otel.disabled) {
return { traces: undefined, metrics: undefined, logs: undefined, warnings: otel.warnings };
return {
traces: undefined,
metrics: undefined,
logs: undefined,
warnings: otel.warnings,
resourceAttributes: otel.resourceAttributes,
};
}

const environment = yield* DesktopEnvironment.DesktopEnvironment;
Expand All @@ -366,6 +372,7 @@ const resolveOtlpEndpoints = Effect.gen(function* () {
metrics: Option.getOrUndefined(environment.otlpMetricsUrl) ?? persisted.otlpMetricsUrl,
logs: Option.getOrUndefined(environment.otlpLogsUrl) ?? persisted.otlpLogsUrl,
warnings: otel.warnings,
resourceAttributes: otel.resourceAttributes,
};
});

Expand Down Expand Up @@ -683,7 +690,10 @@ const telemetryLayer = Layer.unwrap(
Effect.forEach(endpoints.warnings, (warning) => Effect.logWarning(warning)),
);

return otelWarningsLayer.pipe(Layer.provideMerge(Layer.mergeAll(loggerLayer, tracerLayer)));
return otelWarningsLayer.pipe(
Layer.provideMerge(Layer.mergeAll(loggerLayer, tracerLayer)),
Layer.provide(OtelEnvironment.layerResourceAttributes(endpoints.resourceAttributes)),
);
}),
);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vite-plus/test";

import {
filterNewTaskBranches,
resolveNewTaskBranchWorktreePath,
resolveNewTaskBranchLabel,
resolveNewTaskLocalWorkspaceSelection,
Expand Down Expand Up @@ -126,3 +127,26 @@ describe("resolveNewTaskBranchLabel", () => {
).toBe("Choose branch");
});
});

describe("filterNewTaskBranches", () => {
const branches = [
{ name: "main", isRemote: false },
{ name: "Feature/Login-Page", isRemote: false },
{ name: "origin/fix/remote-only", isRemote: true },
];
const search = (query: string) =>
filterNewTaskBranches(branches, query).map((branch) => branch.name);

it("ignores case in both the query and the branch name", () => {
expect(search("feature/login")).toEqual(["Feature/Login-Page"]);
expect(search("MAIN")).toEqual(["main"]);
});

it("keeps remote-only branches searchable", () => {
expect(search("remote-only")).toEqual(["origin/fix/remote-only"]);
});

it("matches a typed space against the dash a branch name uses", () => {
expect(search(" login page ")).toEqual(["Feature/Login-Page"]);
});
});
12 changes: 12 additions & 0 deletions apps/mobile/src/features/threads/new-task-context-presentation.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { sanitizeNewRefName } from "@t3tools/shared/git";

type WorkspaceMode = "local" | "worktree";

export function resolveNewTaskWorkspaceLabel(input: {
Expand Down Expand Up @@ -81,3 +83,13 @@ export function shouldCheckoutNewTaskBranch(input: {
}): boolean {
return input.workspaceMode === "local" && !input.branchIsCurrent && !input.branchWorktreePath;
}

export function filterNewTaskBranches<T extends { readonly name: string }>(
branches: ReadonlyArray<T>,
rawQuery: string,
): ReadonlyArray<T> {
const query = sanitizeNewRefName(rawQuery).toLowerCase();
return query.length === 0
? branches
: branches.filter((branch) => branch.name.toLowerCase().includes(query));
}
25 changes: 12 additions & 13 deletions apps/mobile/src/features/threads/new-task-flow-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
T3_PROJECT_FILE_NAME,
ThreadId,
} from "@t3tools/contracts";
import { sanitizeNewRefName } from "@t3tools/shared/git";
import { resolveProjectSettings } from "@t3tools/shared/projectSettings";
import { parseT3ProjectFile } from "@t3tools/shared/t3ProjectFile";
import * as Arr from "effect/Array";
Expand Down Expand Up @@ -92,6 +93,7 @@ import {
} from "../../state/legacy-plan-mode";
import { useLegacyPlanModeState } from "./use-legacy-plan-mode-enabled";
import {
filterNewTaskBranches,
resolveNewTaskBranchWorktreePath,
resolveNewTaskLocalWorkspaceSelection,
} from "./new-task-context-presentation";
Expand Down Expand Up @@ -129,6 +131,9 @@ export function branchBadgeLabel(input: {
if (input.branch.worktreePath && input.branch.worktreePath !== input.project?.workspaceRoot) {
return "worktree";
}
if (input.branch.isRemote) {
return "remote";
}
if (input.branch.isDefault) {
return "default";
}
Expand Down Expand Up @@ -626,7 +631,8 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
}
replaceComposerDraftAttachments(selectedProjectDraftKey, []);
}, [selectedProjectDraftKey]);
const debouncedBranchQuery = useDebouncedValue(branchQuery, BRANCH_SEARCH_DEBOUNCE_MS);
const branchSearchQuery = sanitizeNewRefName(branchQuery);
const debouncedBranchQuery = useDebouncedValue(branchSearchQuery, BRANCH_SEARCH_DEBOUNCE_MS);
const branchTarget = useMemo(
() => ({
environmentId: selectedProject?.environmentId ?? null,
Expand All @@ -637,7 +643,7 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
[debouncedBranchQuery, selectedProject?.environmentId, selectedProject?.workspaceRoot],
);
const branchState = usePaginatedBranches(branchTarget);
const branchSearchIsDebouncing = branchQuery.trim() !== debouncedBranchQuery.trim();
const branchSearchIsDebouncing = branchSearchQuery !== debouncedBranchQuery;
const branchesLoading =
branchSearchIsDebouncing || (branchState.isPending && branchState.data === null);
const branchesFetchingNextPage = branchState.isFetchingNextPage;
Expand Down Expand Up @@ -669,17 +675,10 @@ export function NewTaskFlowProvider(props: React.PropsWithChildren) {
);
const currentCheckoutBranchName = projectGitStatus.data?.refName ?? null;

const filteredBranches = useMemo(() => {
const query = branchQuery.trim().toLowerCase();
if (query.length === 0) {
return availableBranches;
}

return pipe(
availableBranches,
Arr.filter((branch) => branch.name.toLowerCase().includes(query)),
);
}, [availableBranches, branchQuery]);
const filteredBranches = useMemo(
() => filterNewTaskBranches(allBranchRefs, branchQuery),
[allBranchRefs, branchQuery],
);

// The composer's draft follows the project it will be sent to: switching
// mid-compose keeps the same draft and moves it, so typed text follows the
Expand Down
4 changes: 4 additions & 0 deletions apps/server/src/observability/Layers/Observability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
makeTraceSink,
otlpSerializationLayer,
} from "@t3tools/shared/observability";
import * as OtelEnvironment from "@t3tools/shared/otelEnvironment";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import * as References from "effect/References";
Expand Down Expand Up @@ -96,6 +97,9 @@ export const ObservabilityLive = Layer.unwrap(
Layer.provideMerge(
Layer.mergeAll(ServerLoggerLive, traceReferencesLayer, tracerLayer, metricsLayer),
),
Layer.provide(
OtelEnvironment.layerResourceAttributes(config.otelEnvironment.resourceAttributes),
),
);
}),
);
85 changes: 84 additions & 1 deletion apps/server/src/provider/Drivers/ClaudeDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ import { ServerSettingsService } from "../../serverSettings.ts";
import { ProviderDriverError } from "../Errors.ts";
import { makeClaudeAdapter } from "../Layers/ClaudeAdapter.ts";
import { makeClaudeScopedLimitNames } from "../Layers/claudeUsageLimits.ts";
import * as ClaudeResetCredits from "../Layers/claudeResetCredits.ts";
import * as ResetCreditCoordinator from "../Layers/resetCreditCoordinator.ts";
import {
checkClaudeProviderStatus,
makePendingClaudeProvider,
Expand Down Expand Up @@ -59,7 +61,11 @@ import {
makeProviderSnapshotSettingsSource,
type ProviderSnapshotSettings,
} from "../providerUpdateSettings.ts";
import { makeClaudeCapabilitiesCacheKey, makeClaudeContinuationGroupKey } from "./ClaudeHome.ts";
import {
makeClaudeCapabilitiesCacheKey,
makeClaudeContinuationGroupKey,
resolveClaudeHomePath,
} from "./ClaudeHome.ts";
import { discoverClaudeSkills } from "./ClaudeSkills.ts";
const decodeClaudeSettings = Schema.decodeSync(ClaudeSettings);

Expand Down Expand Up @@ -87,6 +93,7 @@ const UPDATE = makePackageManagedProviderMaintenanceResolver({
export type ClaudeDriverEnv =
| BackgroundPolicy.BackgroundPolicy
| ChildProcessSpawner.ChildProcessSpawner
| ResetCreditCoordinator.ResetCreditCoordinator
| Crypto.Crypto
| FileSystem.FileSystem
| HttpClient.HttpClient
Expand All @@ -111,6 +118,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
const path = yield* Path.Path;
const { cwd } = yield* ServerConfig;
const httpClient = yield* HttpClient.HttpClient;
const resetCreditCoordinator = yield* ResetCreditCoordinator.ResetCreditCoordinator;
const serverSettings = yield* ServerSettingsService;
const eventLoggers = yield* ProviderEventLoggers;
const modelManifest = yield* ModelManifest.ModelManifest;
Expand Down Expand Up @@ -139,6 +147,12 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
effectiveConfig,
processEnv,
);
const configDir = yield* resolveClaudeHomePath(effectiveConfig, processEnv);
const accountConfigPath = yield* ClaudeResetCredits.claudeAccountConfigPath(
effectiveConfig.homePath.trim() || processEnv.CLAUDE_CONFIG_DIR?.trim()
? configDir
: undefined,
);
const stampIdentity = withInstanceIdentity({
instanceId,
driverKind: DRIVER_KIND,
Expand Down Expand Up @@ -193,6 +207,12 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
cwd,
resolveClaudeModelCatalog(manifest),
scopedLimitNames,
(version) =>
ClaudeResetCredits.readClaudeResetCredits(configDir, version).pipe(
Effect.provideService(HttpClient.HttpClient, httpClient),
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(Path.Path, path),
),
),
),
Effect.map(stampIdentity),
Expand Down Expand Up @@ -250,6 +270,68 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
Effect.provideService(Path.Path, path),
);

// Same rules as Codex: serialised on the config directory that holds the
// login, one request id kept until Claude answers (a cooldown or rate
// limit is an answer), then a re-probe.
const consumeResetCredit: NonNullable<ProviderInstance["consumeResetCredit"]> = () =>
Effect.gen(function* () {
const current = yield* snapshot.getSnapshot;
const grantId = current.usageLimits?.resetCredits?.nextCreditId;
if (!grantId || !current.version) return "noCredit" as const;
const version = current.version;
return yield* resetCreditCoordinator.redeem(
configDir,
(requestId) =>
ClaudeResetCredits.consumeClaudeResetCredit({
configDir,
accountConfigPath,
version,
grantId,
requestId,
}),
ClaudeResetCredits.isSettledClaudeResetCreditFailure,
);
}).pipe(
Effect.provideService(HttpClient.HttpClient, httpClient),
Effect.provideService(FileSystem.FileSystem, fileSystem),
Effect.provideService(Path.Path, path),
Effect.mapError(
(cause) =>
new ProviderDriverError({
driver: DRIVER_KIND,
instanceId,
detail:
cause._tag === "ClaudeResetCreditError"
? cause.message
: "Claude could not redeem the reset.",
cause,
}),
),
// Re-probe after any answer, but only a reset claims the limits
// changed, so only a reset reports an unconfirmed refresh.
Effect.tap((outcome) =>
Effect.gen(function* () {
const before = (yield* snapshot.getSnapshot).usageLimits?.checkedAt;
yield* Cache.invalidateAll(capabilitiesProbeCache);
const refreshed = yield* snapshot.refresh;
const after = refreshed.usageLimits?.checkedAt;
if (
outcome === "reset" &&
(after === undefined ||
after === before ||
refreshed.usageLimits?.unavailable?.reason === "probeFailed")
) {
return yield* new ProviderDriverError({
driver: DRIVER_KIND,
instanceId,
detail:
"The reset was applied, but Claude could not confirm the new limits. Refresh to check.",
});
}
}),
),
);

return {
instanceId,
driverKind: DRIVER_KIND,
Expand All @@ -265,6 +347,7 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv> = {
snapshotForCwd,
adapter,
textGeneration,
consumeResetCredit,
} satisfies ProviderInstance;
}),
};
4 changes: 2 additions & 2 deletions apps/server/src/provider/Drivers/CodexDriver.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import * as ChildProcessSpawner from "effect/unstable/process/ChildProcessSpawne
import * as BackgroundPolicy from "../../background/BackgroundPolicy.ts";
import { ServerConfig } from "../../config.ts";
import { ServerSettingsService } from "../../serverSettings.ts";
import { layerTest as codexResetCreditLayerTest } from "../Layers/codexResetCredit.ts";
import * as ResetCreditCoordinator from "../Layers/resetCreditCoordinator.ts";
import { NoOpProviderEventLoggers, ProviderEventLoggers } from "../Layers/ProviderEventLoggers.ts";
import * as ModelManifest from "../ModelManifest.ts";
import {
Expand All @@ -33,7 +33,7 @@ const testLayer = ServerConfig.layerTest(process.cwd(), {
Layer.provideMerge(NodeServices.layer),
Layer.provideMerge(ServerSettingsService.layerTest()),
Layer.provideMerge(ModelManifest.layerTest),
Layer.provideMerge(codexResetCreditLayerTest),
Layer.provideMerge(ResetCreditCoordinator.layerTest),
Layer.provideMerge(
Layer.mock(BackgroundPolicy.BackgroundPolicy)({
shouldRunScopeWork: () => Effect.succeed(false),
Expand Down
Loading
Loading