From d7ee1842533eb4c4cf3e05bea555000592fe9960 Mon Sep 17 00:00:00 2001 From: T3 Code Test Date: Thu, 6 Aug 2026 09:06:02 +0530 Subject: [PATCH 01/12] feat: run Discord coding tasks in isolated worktrees --- apps/server/package.json | 3 + .../src/channels/T3CodeDiscordChannel.test.ts | 42 + .../src/channels/T3CodeDiscordChannel.ts | 500 ++++++++ apps/server/src/server.ts | 7 +- apps/server/src/serverSettings.test.ts | 48 + apps/server/src/serverSettings.ts | 118 +- .../components/settings/ChannelSettings.tsx | 223 ++++ .../settings/SettingsSidebarNav.tsx | 2 + .../src/components/settings/settingsSearch.ts | 7 + apps/web/src/routeTree.gen.ts | 21 + apps/web/src/routes/settings.channels.tsx | 7 + packages/contracts/src/settings.ts | 38 +- pnpm-lock.yaml | 1058 ++++++++++++++++- 13 files changed, 2010 insertions(+), 64 deletions(-) create mode 100644 apps/server/src/channels/T3CodeDiscordChannel.test.ts create mode 100644 apps/server/src/channels/T3CodeDiscordChannel.ts create mode 100644 apps/web/src/components/settings/ChannelSettings.tsx create mode 100644 apps/web/src/routes/settings.channels.tsx diff --git a/apps/server/package.json b/apps/server/package.json index 8e7b5b38591..ed9d6623907 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -23,6 +23,9 @@ }, "dependencies": { "@anthropic-ai/claude-agent-sdk": "^0.3.170", + "@copilotkit/channels-core": "0.7.3", + "@copilotkit/channels-discord": "0.7.3", + "@copilotkit/channels-ui": "0.7.3", "@effect/platform-bun": "catalog:", "@effect/platform-node": "catalog:", "@effect/platform-node-shared": "catalog:", diff --git a/apps/server/src/channels/T3CodeDiscordChannel.test.ts b/apps/server/src/channels/T3CodeDiscordChannel.test.ts new file mode 100644 index 00000000000..0fa33e6440a --- /dev/null +++ b/apps/server/src/channels/T3CodeDiscordChannel.test.ts @@ -0,0 +1,42 @@ +import { expect, it } from "@effect/vitest"; +import { ProjectId } from "@t3tools/contracts"; +import { describe } from "vite-plus/test"; + +import { channelBranchName, isDiscordChannelConfigured } from "./T3CodeDiscordChannel.ts"; + +const configuredDiscord = { + enabled: true, + projectId: ProjectId.make("project-1"), + baseBranch: "main", + branchPrefix: "demo/discord", + applicationId: "app-1", + guildId: "guild-1", + botToken: "token", + botTokenRedacted: true, +} as const; + +describe("Discord channel isolation", () => { + it("creates a unique task branch below the configured prefix", () => { + expect( + channelBranchName({ + prefix: "/demo/discord/", + prompt: "Fix the flaky login test!", + suffix: "a1b2c3d4", + }), + ).toBe("demo/discord/fix-the-flaky-login-test-a1b2c3d4"); + }); + + it("never resolves a task branch to main", () => { + expect( + channelBranchName({ prefix: "demo/discord", prompt: "main", suffix: "12345678" }), + ).not.toBe("main"); + }); + + it("refuses to start without an isolated branch prefix", () => { + expect(isDiscordChannelConfigured({ ...configuredDiscord, branchPrefix: "" })).toBe(false); + }); + + it("refuses to start while the integration is disabled", () => { + expect(isDiscordChannelConfigured({ ...configuredDiscord, enabled: false })).toBe(false); + }); +}); diff --git a/apps/server/src/channels/T3CodeDiscordChannel.ts b/apps/server/src/channels/T3CodeDiscordChannel.ts new file mode 100644 index 00000000000..c153c543e02 --- /dev/null +++ b/apps/server/src/channels/T3CodeDiscordChannel.ts @@ -0,0 +1,500 @@ +import { createChannel } from "@copilotkit/channels-core"; +import { discord } from "@copilotkit/channels-discord"; +import { + Actions, + Button, + Context, + Field, + Fields, + Header, + Message, + Section, +} from "@copilotkit/channels-ui"; +import type { Thread } from "@copilotkit/channels-ui"; +import { + CommandId, + type DiscordChannelSettings, + MessageId, + type ServerSettings, + ThreadId, +} from "@t3tools/contracts"; +import * as Crypto from "effect/Crypto"; +import * as DateTime from "effect/DateTime"; +import * as Effect from "effect/Effect"; +import * as Layer from "effect/Layer"; +import * as Option from "effect/Option"; +import * as Ref from "effect/Ref"; +import * as Stream from "effect/Stream"; + +import { GitWorkflowService } from "../git/GitWorkflowService.ts"; +import { OrchestrationEngineService } from "../orchestration/Services/OrchestrationEngine.ts"; +import { ProjectionSnapshotQuery } from "../orchestration/Services/ProjectionSnapshotQuery.ts"; +import { forkParked } from "../serverActivation.ts"; +import { ServerSettingsService } from "../serverSettings.ts"; + +const DISCORD_ACCENT = "#5865f2"; +const COMPLETED_ACCENT = "#22c55e"; +const FAILED_ACCENT = "#ef4444"; +const MAX_TITLE_LENGTH = 72; +const MAX_BRANCH_SLUG_LENGTH = 40; + +export interface ChannelTaskStatus { + readonly threadId: ThreadId; + readonly title: string; + readonly branch: string; + readonly state: "queued" | "running" | "done" | "failed"; +} + +export interface StartedChannelTask extends ChannelTaskStatus { + readonly state: "queued"; +} + +export interface T3CodeChannelOperations { + readonly startTask: ( + prompt: string, + config: DiscordChannelSettings, + ) => Promise; + readonly getTaskStatus: (threadId: ThreadId) => Promise; +} + +interface LinkedConversationState { + readonly t3ThreadId: string; +} + +type ChannelThread = Pick & { + readonly state: () => Promise; + readonly setState: (value: unknown) => Promise; +}; + +interface ActiveDiscordChannel { + readonly fingerprint: string; + readonly notifyCompleted: (input: { + readonly threadId: ThreadId; + readonly changedFileCount: number; + }) => Promise; + readonly stop: () => Promise; +} + +export function isDiscordChannelConfigured(config: DiscordChannelSettings): boolean { + return ( + config.enabled && + config.projectId !== null && + config.baseBranch.trim().length > 0 && + config.branchPrefix.trim().length > 0 && + config.applicationId.length > 0 && + config.botToken.length > 0 + ); +} + +export function channelBranchName(input: { + readonly prefix: string; + readonly prompt: string; + readonly suffix: string; +}): string { + const slug = input.prompt + .toLocaleLowerCase() + .replace(/[^a-z0-9]+/gu, "-") + .replace(/^-+|-+$/gu, "") + .slice(0, MAX_BRANCH_SLUG_LENGTH) + .replace(/-+$/gu, ""); + const prefix = input.prefix.replace(/^\/+|\/+$/gu, ""); + return `${prefix}/${slug || "task"}-${input.suffix}`; +} + +function promptTitle(prompt: string): string { + const singleLine = prompt.replace(/\s+/gu, " ").trim(); + return singleLine.length <= MAX_TITLE_LENGTH + ? singleLine + : `${singleLine.slice(0, MAX_TITLE_LENGTH - 1).trimEnd()}…`; +} + +function cleanDiscordPrompt(input: string): string { + return input.replace(/<@!?\d+>/gu, "").trim(); +} + +function taskStateLabel(state: ChannelTaskStatus["state"]): string { + switch (state) { + case "queued": + return "Queued"; + case "running": + return "Running"; + case "done": + return "Done"; + case "failed": + return "Failed"; + } +} + +function statusCard(status: ChannelTaskStatus) { + return Message({ + accent: status.state === "failed" ? FAILED_ACCENT : DISCORD_ACCENT, + fallbackText: `${status.title}: ${taskStateLabel(status.state)}`, + children: [ + Header({ children: status.title }), + Fields({ + children: [ + Field({ label: "Status", children: taskStateLabel(status.state) }), + Field({ label: "Branch", children: `\`${status.branch}\`` }), + ], + }), + Context({ + children: + "This task is running in an isolated worktree. The base branch is never checked out for agent work.", + }), + ], + }); +} + +function startedCard( + task: StartedChannelTask, + onStatus: (thread: Pick) => Promise, +) { + return Message({ + accent: DISCORD_ACCENT, + fallbackText: `T3 Code started: ${task.title}`, + children: [ + Header({ children: "T3 Code task started" }), + Section({ children: task.title }), + Fields({ + children: [ + Field({ label: "Status", children: "Queued" }), + Field({ label: "Branch", children: `\`${task.branch}\`` }), + ], + }), + Actions({ + children: Button({ + style: "primary", + value: task.threadId, + onClick: ({ thread }) => onStatus(thread), + children: "Check status", + }), + }), + Context({ children: "T3 Code will reply here when the run and diff are complete." }), + ], + }); +} + +function completedCard(input: { + readonly task: ChannelTaskStatus; + readonly changedFileCount: number; +}) { + const fileLabel = `${input.changedFileCount} changed ${input.changedFileCount === 1 ? "file" : "files"}`; + return Message({ + accent: COMPLETED_ACCENT, + fallbackText: `T3 Code finished: ${input.task.title}`, + children: [ + Header({ children: "T3 Code finished" }), + Section({ children: input.task.title }), + Fields({ + children: [ + Field({ label: "Status", children: "Done" }), + Field({ label: "Diff", children: fileLabel }), + Field({ label: "Branch", children: `\`${input.task.branch}\`` }), + ], + }), + Context({ children: "Open T3 Code to inspect the full transcript and diff." }), + ], + }); +} + +function createT3CodeChannel(input: { + readonly config: DiscordChannelSettings; + readonly operations: T3CodeChannelOperations; +}) { + const linkedThreads = new Map>(); + const channel = createChannel({ + name: "t3-code", + identifyUser: "platform", + adapters: [ + discord({ + botToken: input.config.botToken, + appId: input.config.applicationId, + ...(input.config.guildId.length > 0 ? { guildId: input.config.guildId } : {}), + }), + ], + }); + + const postStatus = async (thread: Pick, threadId: ThreadId) => { + const status = await input.operations.getTaskStatus(threadId); + await thread.post( + status + ? statusCard(status) + : Message({ + accent: FAILED_ACCENT, + children: Section({ children: "That T3 Code task no longer exists." }), + }), + ); + }; + + const handleText = async (thread: ChannelThread, rawText: string) => { + const text = cleanDiscordPrompt(rawText); + const storedState = await thread.state(); + const state = + typeof storedState === "object" && + storedState !== null && + "t3ThreadId" in storedState && + typeof storedState.t3ThreadId === "string" + ? ({ t3ThreadId: storedState.t3ThreadId } satisfies LinkedConversationState) + : undefined; + if (text.toLocaleLowerCase() === "status") { + if (!state?.t3ThreadId) { + await thread.post("No T3 Code task is linked to this Discord thread yet."); + return; + } + await postStatus(thread, ThreadId.make(state.t3ThreadId)); + return; + } + if (text.length === 0) { + await thread.post("Mention me with a coding task, or send `status` to check the linked run."); + return; + } + + if (state?.t3ThreadId) { + const current = await input.operations.getTaskStatus(ThreadId.make(state.t3ThreadId)); + if (current?.state === "queued" || current?.state === "running") { + await thread.post(statusCard(current)); + return; + } + } + + try { + const task = await input.operations.startTask(text, input.config); + await thread.setState({ t3ThreadId: task.threadId } satisfies LinkedConversationState); + linkedThreads.set(task.threadId, thread); + await thread.post(startedCard(task, (target) => postStatus(target, task.threadId))); + } catch { + await thread.post( + Message({ + accent: FAILED_ACCENT, + fallbackText: "T3 Code could not start this task.", + children: [ + Header({ children: "Task did not start" }), + Section({ + children: + "T3 Code could not create an isolated worktree. The task was stopped before the agent ran.", + }), + ], + }), + ); + } + }; + + channel.onMention(({ thread, message }) => handleText(thread, message.text)); + channel.onCommand("t3", ({ thread, text }) => handleText(thread, text)); + + return { + channel, + notifyCompleted: async (completion: { + readonly threadId: ThreadId; + readonly changedFileCount: number; + }) => { + const thread = linkedThreads.get(completion.threadId); + if (!thread) return; + const task = await input.operations.getTaskStatus(completion.threadId); + if (!task) return; + await thread.post(completedCard({ task, changedFileCount: completion.changedFileCount })); + linkedThreads.delete(completion.threadId); + }, + }; +} + +const makeOperations = Effect.gen(function* () { + const crypto = yield* Crypto.Crypto; + const gitWorkflow = yield* GitWorkflowService; + const orchestrationEngine = yield* OrchestrationEngineService; + const projectionSnapshotQuery = yield* ProjectionSnapshotQuery; + const runtimeContext = yield* Effect.context(); + const runPromise = Effect.runPromiseWith(runtimeContext); + + const nextId = Effect.fn("T3CodeDiscordChannel.nextId")(function* (prefix: string) { + const uuid = yield* crypto.randomUUIDv4; + return `${prefix}-${uuid}`; + }); + + const startTaskEffect = Effect.fn("T3CodeDiscordChannel.startTask")(function* ( + prompt: string, + config: DiscordChannelSettings, + ) { + if (config.projectId === null) { + return yield* Effect.fail("Discord channel project is not configured"); + } + const projectOption = yield* projectionSnapshotQuery.getProjectShellById(config.projectId); + if (Option.isNone(projectOption)) { + return yield* Effect.fail("Discord channel project was not found"); + } + const project = projectOption.value; + if (project.defaultModelSelection === null) { + return yield* Effect.fail("Discord channel project has no default model"); + } + + const now = DateTime.formatIso(yield* DateTime.now); + const threadId = ThreadId.make(yield* nextId("channel-thread")); + const suffix = (yield* nextId("branch")).slice(-8); + const branch = channelBranchName({ + prefix: config.branchPrefix, + prompt, + suffix, + }); + if (branch === config.baseBranch) { + return yield* Effect.fail("Discord channel branch must differ from its base branch"); + } + + const worktree = yield* gitWorkflow.createWorktree({ + cwd: project.workspaceRoot, + refName: config.baseBranch, + baseRefName: config.baseBranch, + newRefName: branch, + path: null, + }); + const title = promptTitle(prompt); + yield* orchestrationEngine.dispatch({ + type: "thread.create", + commandId: CommandId.make(yield* nextId("channel-create")), + threadId, + projectId: project.id, + title, + modelSelection: project.defaultModelSelection, + runtimeMode: "full-access", + interactionMode: "default", + branch: worktree.worktree.refName, + worktreePath: worktree.worktree.path, + createdAt: now, + }); + yield* orchestrationEngine.dispatch({ + type: "thread.turn.start", + commandId: CommandId.make(yield* nextId("channel-turn")), + threadId, + message: { + messageId: MessageId.make(yield* nextId("channel-message")), + role: "user", + text: prompt, + attachments: [], + }, + runtimeMode: "full-access", + interactionMode: "default", + createdAt: now, + }); + return { + threadId, + title, + branch: worktree.worktree.refName, + state: "queued" as const, + }; + }); + + const getTaskStatusEffect = Effect.fn("T3CodeDiscordChannel.getTaskStatus")(function* ( + threadId: ThreadId, + ) { + const threadOption = yield* projectionSnapshotQuery.getThreadShellById(threadId); + if (Option.isNone(threadOption)) return null; + const thread = threadOption.value; + const state = (() => { + if (thread.latestTurn?.state === "error" || thread.session?.status === "error") { + return "failed" as const; + } + if (thread.latestTurn?.state === "completed") return "done" as const; + if (thread.latestTurn?.state === "running" || thread.session?.status === "running") { + return "running" as const; + } + return "queued" as const; + })(); + return { + threadId, + title: thread.title, + branch: thread.branch ?? "isolated worktree", + state, + }; + }); + + return { + startTask: (prompt, config) => runPromise(startTaskEffect(prompt, config)), + getTaskStatus: (threadId) => runPromise(getTaskStatusEffect(threadId)), + } satisfies T3CodeChannelOperations; +}); + +function configFingerprint(config: DiscordChannelSettings): string { + return [ + config.enabled, + config.projectId, + config.baseBranch, + config.branchPrefix, + config.applicationId, + config.guildId, + config.botToken, + ].join("\u0000"); +} + +export const layer = Layer.effectDiscard( + Effect.gen(function* () { + const settingsService = yield* ServerSettingsService; + const orchestrationEngine = yield* OrchestrationEngineService; + const operations = yield* makeOperations; + const activeRef = yield* Ref.make(null); + + const stopActive = Effect.fn("T3CodeDiscordChannel.stopActive")(function* () { + const active = yield* Ref.getAndSet(activeRef, null); + if (!active) return; + yield* Effect.tryPromise(() => active.stop()).pipe(Effect.ignoreCause({ log: true })); + }); + + const reconcile = Effect.fn("T3CodeDiscordChannel.reconcile")(function* ( + settings: ServerSettings, + ) { + const config = settings.channelIntegrations.discord; + const fingerprint = configFingerprint(config); + const active = yield* Ref.get(activeRef); + if (active?.fingerprint === fingerprint) return; + yield* stopActive(); + if (!isDiscordChannelConfigured(config)) return; + + const created = createT3CodeChannel({ config, operations }); + const connected = yield* Effect.tryPromise(() => created.channel.ɵruntime.start()).pipe( + Effect.timeout("15 seconds"), + Effect.as(true), + Effect.tapCause((cause) => + Effect.logWarning("Discord channel failed to connect", { cause }), + ), + Effect.catchCause(() => Effect.succeed(false)), + ); + if (!connected) { + yield* Effect.tryPromise(() => created.channel.ɵruntime.stop()).pipe( + Effect.ignoreCause({ log: true }), + ); + return; + } + yield* Ref.set(activeRef, { + fingerprint, + notifyCompleted: created.notifyCompleted, + stop: () => created.channel.ɵruntime.stop(), + }); + }); + + yield* Effect.addFinalizer(() => stopActive()); + yield* forkParked( + Effect.scoped( + Effect.gen(function* () { + const changes = yield* settingsService.subscribeChanges; + yield* reconcile(yield* settingsService.getSettings); + yield* Stream.runForEach(changes, reconcile); + }), + ), + ); + yield* forkParked( + Stream.runForEach(orchestrationEngine.streamDomainEvents, (event) => { + if (event.type !== "thread.turn-diff-completed") return Effect.void; + return Ref.get(activeRef).pipe( + Effect.flatMap((active) => + active + ? Effect.tryPromise(() => + active.notifyCompleted({ + threadId: event.payload.threadId, + changedFileCount: event.payload.files.length, + }), + ).pipe(Effect.ignoreCause({ log: true })) + : Effect.void, + ), + ); + }), + ); + }), +); diff --git a/apps/server/src/server.ts b/apps/server/src/server.ts index 49a3a31940f..7ddd7a30fae 100644 --- a/apps/server/src/server.ts +++ b/apps/server/src/server.ts @@ -65,6 +65,7 @@ import * as RepositoryIdentityResolver from "./project/RepositoryIdentityResolve import * as WorkspaceEntries from "./workspace/WorkspaceEntries.ts"; import * as WorkspaceFileSystem from "./workspace/WorkspaceFileSystem.ts"; import * as WorkspacePaths from "./workspace/WorkspacePaths.ts"; +import * as T3CodeDiscordChannel from "./channels/T3CodeDiscordChannel.ts"; import * as GitVcsDriver from "./vcs/GitVcsDriver.ts"; import * as VcsDriverRegistry from "./vcs/VcsDriverRegistry.ts"; import * as VcsProjectConfig from "./vcs/VcsProjectConfig.ts"; @@ -405,7 +406,11 @@ const RuntimeCoreDependenciesLive = ReactorLayerLive.pipe( ), ); -const RuntimeDependenciesLive = RuntimeCoreDependenciesLive.pipe( +const RuntimeCoreWithChannelsLive = T3CodeDiscordChannel.layer.pipe( + Layer.provideMerge(RuntimeCoreDependenciesLive), +); + +const RuntimeDependenciesLive = RuntimeCoreWithChannelsLive.pipe( // Misc. Layer.provideMerge(BackgroundLayerLive), Layer.provideMerge(ResourceDiagnosticsLayerLive), diff --git a/apps/server/src/serverSettings.test.ts b/apps/server/src/serverSettings.test.ts index d38a3064910..fe45faa2343 100644 --- a/apps/server/src/serverSettings.test.ts +++ b/apps/server/src/serverSettings.test.ts @@ -1,6 +1,7 @@ import * as NodeServices from "@effect/platform-node/NodeServices"; import { DEFAULT_SERVER_SETTINGS, + ProjectId, ProviderDriverKind, ProviderInstanceId, ServerSettings, @@ -691,4 +692,51 @@ it.layer(NodeServices.layer)("server settings", (it) => { ); }).pipe(Effect.provide(makeServerSettingsLayer())), ); + + it.effect("stores Discord channel credentials outside settings.json", () => + Effect.gen(function* () { + const serverSettings = yield* ServerSettingsModule.ServerSettingsService; + const serverConfig = yield* ServerConfig.ServerConfig; + const fileSystem = yield* FileSystem.FileSystem; + + const next = yield* serverSettings.updateSettings({ + channelIntegrations: { + discord: { + enabled: true, + projectId: ProjectId.make("project-1"), + baseBranch: "main", + branchPrefix: "demo/discord", + applicationId: "app-1", + guildId: "guild-1", + botToken: "discord-secret", + botTokenRedacted: false, + }, + }, + }); + + assert.equal(next.channelIntegrations.discord.botToken, "discord-secret"); + + const raw = yield* fileSystem.readFileString(serverConfig.settingsPath); + assert.notInclude(raw, "discord-secret"); + // @effect-diagnostics-next-line preferSchemaOverJson:off + const persisted = JSON.parse(raw).channelIntegrations.discord; + assert.equal(persisted.botToken, ""); + assert.equal(persisted.botTokenRedacted, true); + + const roundTripped = yield* serverSettings.updateSettings({ + channelIntegrations: { + discord: { + ...next.channelIntegrations.discord, + botToken: "", + botTokenRedacted: true, + }, + }, + }); + assert.equal(roundTripped.channelIntegrations.discord.botToken, "discord-secret"); + + const clientSettings = ServerSettingsModule.redactServerSettingsForClient(roundTripped); + assert.equal(clientSettings.channelIntegrations.discord.botToken, ""); + assert.equal(clientSettings.channelIntegrations.discord.botTokenRedacted, true); + }).pipe(Effect.provide(makeServerSettingsLayer())), + ); }); diff --git a/apps/server/src/serverSettings.ts b/apps/server/src/serverSettings.ts index 2798faf6f00..f133ae69e2a 100644 --- a/apps/server/src/serverSettings.ts +++ b/apps/server/src/serverSettings.ts @@ -83,6 +83,8 @@ function providerEnvironmentSecretName(input: { return `provider-env-${Buffer.from(input.instanceId, "utf8").toString("base64url")}-${Buffer.from(input.name, "utf8").toString("base64url")}`; } +const DISCORD_BOT_TOKEN_SECRET_NAME = "channel-discord-bot-token"; + function redactProviderEnvironmentVariable( variable: ProviderInstanceEnvironmentVariable, ): ProviderInstanceEnvironmentVariable { @@ -109,7 +111,19 @@ export function redactServerSettingsForClient(settings: ServerSettings): ServerS : instance, ]), ); - return { ...settings, providerInstances }; + const discord = settings.channelIntegrations.discord; + return { + ...settings, + providerInstances, + channelIntegrations: { + ...settings.channelIntegrations, + discord: { + ...discord, + botToken: "", + botTokenRedacted: discord.botToken.length > 0 || discord.botTokenRedacted, + }, + }, + }; } export class ServerSettingsService extends Context.Service< @@ -214,6 +228,7 @@ const ATOMIC_SETTINGS_KEYS: ReadonlySet = new Set([ "providerHealthRefreshInterval", "sourceControlWriterModelSelection", "textGenerationModelSelection", + "channelIntegrations", ]); function stripDefaultServerSettings(current: unknown, defaults: unknown): unknown | undefined { @@ -358,10 +373,46 @@ const make = Effect.gen(function* () { }; }); + const materializeChannelSecrets = ( + settings: ServerSettings, + ): Effect.Effect => + Effect.gen(function* () { + const discord = settings.channelIntegrations.discord; + const readSecret = (name: string) => + secretStore.get(name).pipe( + Effect.mapError( + (cause) => + new ServerSettingsError({ + settingsPath, + operation: "read-secret", + cause, + }), + ), + Effect.map(Option.map((value) => textDecoder.decode(value))), + Effect.map(Option.getOrElse(() => "")), + ); + const botToken = discord.botTokenRedacted + ? yield* readSecret(DISCORD_BOT_TOKEN_SECRET_NAME) + : discord.botToken; + return { + ...settings, + channelIntegrations: { + ...settings.channelIntegrations, + discord: { + ...discord, + botToken, + }, + }, + }; + }); + + const materializeServerSecrets = (settings: ServerSettings) => + materializeProviderEnvironmentSecrets(settings).pipe(Effect.flatMap(materializeChannelSecrets)); + const materializeChanges = (changes: Stream.Stream) => changes.pipe( Stream.mapEffect((settings) => - materializeProviderEnvironmentSecrets(settings).pipe( + materializeServerSecrets(settings).pipe( Effect.catch((error: ServerSettingsError) => Effect.logWarning("failed to materialize provider environment secrets", { operation: error.operation, @@ -476,6 +527,62 @@ const make = Effect.gen(function* () { }; }); + const persistChannelSecrets = ( + next: ServerSettings, + ): Effect.Effect => + Effect.gen(function* () { + const discord = next.channelIntegrations.discord; + const persistSecret = Effect.fn("ServerSettings.persistChannelSecret")(function* (input: { + readonly name: string; + readonly value: string; + readonly redacted: boolean; + }) { + if (input.redacted) { + return { value: "", redacted: true } as const; + } + if (input.value.length === 0) { + yield* secretStore.remove(input.name).pipe( + Effect.mapError( + (cause) => + new ServerSettingsError({ + settingsPath, + operation: "remove-secret", + cause, + }), + ), + ); + return { value: "", redacted: false } as const; + } + yield* secretStore.set(input.name, textEncoder.encode(input.value)).pipe( + Effect.mapError( + (cause) => + new ServerSettingsError({ + settingsPath, + operation: "write-secret", + cause, + }), + ), + ); + return { value: "", redacted: true } as const; + }); + const botToken = yield* persistSecret({ + name: DISCORD_BOT_TOKEN_SECRET_NAME, + value: discord.botToken, + redacted: discord.botTokenRedacted, + }); + return { + ...next, + channelIntegrations: { + ...next.channelIntegrations, + discord: { + ...discord, + botToken: botToken.value, + botTokenRedacted: botToken.redacted, + }, + }, + }; + }); + const writeSettingsAtomically = Effect.fnUntraced( function* (settings: ServerSettings) { const sparseSettingsJson = yield* encodeServerSettingsJson( @@ -572,22 +679,23 @@ const make = Effect.gen(function* () { start, ready: Deferred.await(startedDeferred), getSettings: getSettingsFromCache.pipe( - Effect.flatMap(materializeProviderEnvironmentSecrets), + Effect.flatMap(materializeServerSecrets), Effect.map(resolveTextGenerationProvider), ), updateSettings: (patch) => writeSemaphore.withPermits(1)( Effect.gen(function* () { const current = yield* getSettingsFromCache; - const nextPersisted = yield* persistProviderEnvironmentSecrets( + const nextProviderSecretsPersisted = yield* persistProviderEnvironmentSecrets( current, applyServerSettingsPatch(current, patch), ); + const nextPersisted = yield* persistChannelSecrets(nextProviderSecretsPersisted); const next = yield* normalizeServerSettings(nextPersisted); yield* writeSettingsAtomically(next); yield* Cache.set(settingsCache, cacheKey, next); yield* emitChange(next); - const materialized = yield* materializeProviderEnvironmentSecrets(next); + const materialized = yield* materializeServerSecrets(next); return resolveTextGenerationProvider(materialized); }), ), diff --git a/apps/web/src/components/settings/ChannelSettings.tsx b/apps/web/src/components/settings/ChannelSettings.tsx new file mode 100644 index 00000000000..c1bd16c6518 --- /dev/null +++ b/apps/web/src/components/settings/ChannelSettings.tsx @@ -0,0 +1,223 @@ +import { BotIcon, GitBranchIcon, ShieldCheckIcon } from "lucide-react"; +import { ProjectId } from "@t3tools/contracts"; +import { useEffect, useMemo, useState } from "react"; + +import { usePrimarySettings, useUpdatePrimarySettings } from "../../hooks/useSettings"; +import { useProjects } from "../../state/entities"; +import { usePrimaryEnvironment } from "../../state/environments"; +import { Badge } from "../ui/badge"; +import { Button } from "../ui/button"; +import { Input } from "../ui/input"; +import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ui/select"; +import { Switch } from "../ui/switch"; +import { SettingsPageContainer, SettingsRow, SettingsSection } from "./settingsLayout"; +import { searchableSetting } from "./settingsSearch"; + +function SecretInput({ + label, + stored, + value, + onChange, +}: { + readonly label: string; + readonly stored: boolean; + readonly value: string; + readonly onChange: (value: string) => void; +}) { + return ( + onChange(event.currentTarget.value)} + placeholder={stored ? "Stored securely — type to replace" : label} + aria-label={label} + /> + ); +} + +export function ChannelSettings() { + const settings = usePrimarySettings((value) => value.channelIntegrations.discord); + const updateSettings = useUpdatePrimarySettings(); + const primaryEnvironment = usePrimaryEnvironment(); + const allProjects = useProjects(); + const projects = useMemo( + () => + primaryEnvironment + ? allProjects.filter( + (project) => project.environmentId === primaryEnvironment.environmentId, + ) + : [], + [allProjects, primaryEnvironment], + ); + const [enabled, setEnabled] = useState(settings.enabled); + const [projectId, setProjectId] = useState(settings.projectId); + const [baseBranch, setBaseBranch] = useState(settings.baseBranch); + const [branchPrefix, setBranchPrefix] = useState(settings.branchPrefix); + const [applicationId, setApplicationId] = useState(settings.applicationId); + const [guildId, setGuildId] = useState(settings.guildId); + const [botToken, setBotToken] = useState(""); + const [botTokenChanged, setBotTokenChanged] = useState(false); + + useEffect(() => { + setEnabled(settings.enabled); + setProjectId(settings.projectId); + setBaseBranch(settings.baseBranch); + setBranchPrefix(settings.branchPrefix); + setApplicationId(settings.applicationId); + setGuildId(settings.guildId); + }, [settings]); + + const hasBotToken = botTokenChanged ? botToken.length > 0 : settings.botTokenRedacted; + const setupComplete = + projectId !== null && + baseBranch.trim().length > 0 && + branchPrefix.trim().length > 0 && + applicationId.trim().length > 0 && + hasBotToken; + const selectedProject = projects.find((project) => project.id === projectId) ?? null; + + const save = () => { + updateSettings({ + channelIntegrations: { + discord: { + enabled, + projectId, + baseBranch, + branchPrefix, + applicationId, + guildId, + botToken: botTokenChanged ? botToken : "", + botTokenRedacted: botTokenChanged ? false : settings.botTokenRedacted, + }, + }, + }); + setBotToken(""); + setBotTokenChanged(false); + }; + + return ( + + } + headerAction={ + + {enabled && setupComplete ? "Configured" : enabled ? "Setup incomplete" : "Off"} + + } + > + setEnabled(Boolean(checked))} + aria-label="Enable Discord channel" + /> + } + /> + setProjectId(value ? ProjectId.make(value) : null)} + > + + + {selectedProject?.title ?? + (projects.length > 0 ? "Choose a project" : "No projects")} + + + + {projects.map((project) => ( + + {project.title} + + ))} + + + } + /> + +
+ setApplicationId(event.currentTarget.value)} + placeholder="Application ID" + aria-label="Discord application ID" + /> + setGuildId(event.currentTarget.value)} + placeholder="Server ID (optional)" + aria-label="Discord server ID" + /> + { + setBotToken(value); + setBotTokenChanged(true); + }} + /> +
+
+
+ + }> + + Base branch protected + + } + /> + +
+
+ + setBaseBranch(event.currentTarget.value)} + placeholder="main" + aria-label="Channel base branch" + /> +
+
+ + setBranchPrefix(event.currentTarget.value)} + placeholder="demo/discord" + aria-label="Channel branch prefix" + /> +
+
+
+
+ +
+
+
+ ); +} diff --git a/apps/web/src/components/settings/SettingsSidebarNav.tsx b/apps/web/src/components/settings/SettingsSidebarNav.tsx index b7ca9afcf83..88bac823819 100644 --- a/apps/web/src/components/settings/SettingsSidebarNav.tsx +++ b/apps/web/src/components/settings/SettingsSidebarNav.tsx @@ -15,6 +15,7 @@ import { GitBranchIcon, KeyboardIcon, Link2Icon, + MessagesSquareIcon, PaletteIcon, SearchIcon, Settings2Icon, @@ -51,6 +52,7 @@ const SETTINGS_SECTION_ICONS: Readonly< "/settings/keybindings": KeyboardIcon, "/settings/providers": BotIcon, "/settings/source-control": GitBranchIcon, + "/settings/channels": MessagesSquareIcon, "/settings/connections": Link2Icon, "/settings/beta": FlaskConicalIcon, "/settings/archived": ArchiveIcon, diff --git a/apps/web/src/components/settings/settingsSearch.ts b/apps/web/src/components/settings/settingsSearch.ts index 1ba231a5835..9e00714038a 100644 --- a/apps/web/src/components/settings/settingsSearch.ts +++ b/apps/web/src/components/settings/settingsSearch.ts @@ -4,6 +4,7 @@ export type SettingsPath = | "/settings/keybindings" | "/settings/providers" | "/settings/source-control" + | "/settings/channels" | "/settings/connections" | "/settings/beta" | "/settings/archived"; @@ -25,6 +26,7 @@ export const SETTINGS_SECTION_LABELS: Readonly> = { "/settings/keybindings": "Keybindings", "/settings/providers": "Providers", "/settings/source-control": "Source Control", + "/settings/channels": "Channels", "/settings/connections": "Connections", "/settings/beta": "Beta", "/settings/archived": "Archive", @@ -166,6 +168,11 @@ export const SETTINGS_SEARCH_ITEMS = [ title: "Source control", to: "/settings/source-control", }, + { + id: "discord-channel", + title: "Discord channel", + to: "/settings/channels", + }, { id: "remote-environments", title: "Remote environments", diff --git a/apps/web/src/routeTree.gen.ts b/apps/web/src/routeTree.gen.ts index 58ab4c3a714..bd09c7d4bd6 100644 --- a/apps/web/src/routeTree.gen.ts +++ b/apps/web/src/routeTree.gen.ts @@ -20,6 +20,7 @@ import { Route as SettingsKeybindingsRouteImport } from './routes/settings.keybi import { Route as SettingsGeneralRouteImport } from './routes/settings.general' import { Route as SettingsDiagnosticsRouteImport } from './routes/settings.diagnostics' import { Route as SettingsConnectionsRouteImport } from './routes/settings.connections' +import { Route as SettingsChannelsRouteImport } from './routes/settings.channels' import { Route as SettingsBetaRouteImport } from './routes/settings.beta' import { Route as SettingsArchivedRouteImport } from './routes/settings.archived' import { Route as SettingsAppearanceRouteImport } from './routes/settings.appearance' @@ -81,6 +82,11 @@ const SettingsConnectionsRoute = SettingsConnectionsRouteImport.update({ path: '/connections', getParentRoute: () => SettingsRoute, } as any) +const SettingsChannelsRoute = SettingsChannelsRouteImport.update({ + id: '/channels', + path: '/channels', + getParentRoute: () => SettingsRoute, +} as any) const SettingsBetaRoute = SettingsBetaRouteImport.update({ id: '/beta', path: '/beta', @@ -122,6 +128,7 @@ export interface FileRoutesByFullPath { '/settings/appearance': typeof SettingsAppearanceRoute '/settings/archived': typeof SettingsArchivedRoute '/settings/beta': typeof SettingsBetaRoute + '/settings/channels': typeof SettingsChannelsRoute '/settings/connections': typeof SettingsConnectionsRoute '/settings/diagnostics': typeof SettingsDiagnosticsRoute '/settings/general': typeof SettingsGeneralRoute @@ -139,6 +146,7 @@ export interface FileRoutesByTo { '/settings/appearance': typeof SettingsAppearanceRoute '/settings/archived': typeof SettingsArchivedRoute '/settings/beta': typeof SettingsBetaRoute + '/settings/channels': typeof SettingsChannelsRoute '/settings/connections': typeof SettingsConnectionsRoute '/settings/diagnostics': typeof SettingsDiagnosticsRoute '/settings/general': typeof SettingsGeneralRoute @@ -159,6 +167,7 @@ export interface FileRoutesById { '/settings/appearance': typeof SettingsAppearanceRoute '/settings/archived': typeof SettingsArchivedRoute '/settings/beta': typeof SettingsBetaRoute + '/settings/channels': typeof SettingsChannelsRoute '/settings/connections': typeof SettingsConnectionsRoute '/settings/diagnostics': typeof SettingsDiagnosticsRoute '/settings/general': typeof SettingsGeneralRoute @@ -180,6 +189,7 @@ export interface FileRouteTypes { | '/settings/appearance' | '/settings/archived' | '/settings/beta' + | '/settings/channels' | '/settings/connections' | '/settings/diagnostics' | '/settings/general' @@ -197,6 +207,7 @@ export interface FileRouteTypes { | '/settings/appearance' | '/settings/archived' | '/settings/beta' + | '/settings/channels' | '/settings/connections' | '/settings/diagnostics' | '/settings/general' @@ -216,6 +227,7 @@ export interface FileRouteTypes { | '/settings/appearance' | '/settings/archived' | '/settings/beta' + | '/settings/channels' | '/settings/connections' | '/settings/diagnostics' | '/settings/general' @@ -314,6 +326,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof SettingsConnectionsRouteImport parentRoute: typeof SettingsRoute } + '/settings/channels': { + id: '/settings/channels' + path: '/channels' + fullPath: '/settings/channels' + preLoaderRoute: typeof SettingsChannelsRouteImport + parentRoute: typeof SettingsRoute + } '/settings/beta': { id: '/settings/beta' path: '/beta' @@ -377,6 +396,7 @@ interface SettingsRouteChildren { SettingsAppearanceRoute: typeof SettingsAppearanceRoute SettingsArchivedRoute: typeof SettingsArchivedRoute SettingsBetaRoute: typeof SettingsBetaRoute + SettingsChannelsRoute: typeof SettingsChannelsRoute SettingsConnectionsRoute: typeof SettingsConnectionsRoute SettingsDiagnosticsRoute: typeof SettingsDiagnosticsRoute SettingsGeneralRoute: typeof SettingsGeneralRoute @@ -389,6 +409,7 @@ const SettingsRouteChildren: SettingsRouteChildren = { SettingsAppearanceRoute: SettingsAppearanceRoute, SettingsArchivedRoute: SettingsArchivedRoute, SettingsBetaRoute: SettingsBetaRoute, + SettingsChannelsRoute: SettingsChannelsRoute, SettingsConnectionsRoute: SettingsConnectionsRoute, SettingsDiagnosticsRoute: SettingsDiagnosticsRoute, SettingsGeneralRoute: SettingsGeneralRoute, diff --git a/apps/web/src/routes/settings.channels.tsx b/apps/web/src/routes/settings.channels.tsx new file mode 100644 index 00000000000..6230dc0faf5 --- /dev/null +++ b/apps/web/src/routes/settings.channels.tsx @@ -0,0 +1,7 @@ +import { createFileRoute } from "@tanstack/react-router"; + +import { ChannelSettings } from "../components/settings/ChannelSettings"; + +export const Route = createFileRoute("/settings/channels")({ + component: ChannelSettings, +}); diff --git a/packages/contracts/src/settings.ts b/packages/contracts/src/settings.ts index cbb547b95fb..76d7cbb8fe0 100644 --- a/packages/contracts/src/settings.ts +++ b/packages/contracts/src/settings.ts @@ -2,7 +2,7 @@ import * as Effect from "effect/Effect"; import * as Duration from "effect/Duration"; import * as Schema from "effect/Schema"; import * as SchemaTransformation from "effect/SchemaTransformation"; -import { TrimmedNonEmptyString, TrimmedString } from "./baseSchemas.ts"; +import { ProjectId, TrimmedNonEmptyString, TrimmedString } from "./baseSchemas.ts"; import { DEFAULT_TEXT_GENERATION_MODEL, DEFAULT_TEXT_GENERATION_REASONING_EFFORT, @@ -534,6 +534,25 @@ export const BackgroundActivitySettings = Schema.Struct({ }).pipe(Schema.withDecodingDefault(Effect.succeed({}))); export type BackgroundActivitySettings = typeof BackgroundActivitySettings.Type; +export const DiscordChannelSettings = Schema.Struct({ + enabled: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), + projectId: Schema.NullOr(ProjectId).pipe(Schema.withDecodingDefault(Effect.succeed(null))), + baseBranch: TrimmedNonEmptyString.pipe(Schema.withDecodingDefault(Effect.succeed("main"))), + branchPrefix: TrimmedNonEmptyString.pipe( + Schema.withDecodingDefault(Effect.succeed("demo/discord")), + ), + applicationId: TrimmedString.pipe(Schema.withDecodingDefault(Effect.succeed(""))), + guildId: TrimmedString.pipe(Schema.withDecodingDefault(Effect.succeed(""))), + botToken: TrimmedString.pipe(Schema.withDecodingDefault(Effect.succeed(""))), + botTokenRedacted: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), +}); +export type DiscordChannelSettings = typeof DiscordChannelSettings.Type; + +export const ChannelIntegrationSettings = Schema.Struct({ + discord: DiscordChannelSettings.pipe(Schema.withDecodingDefault(Effect.succeed({}))), +}).pipe(Schema.withDecodingDefault(Effect.succeed({}))); +export type ChannelIntegrationSettings = typeof ChannelIntegrationSettings.Type; + export const ServerSettings = Schema.Struct({ enableAssistantStreaming: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(false))), enableProviderUpdateChecks: Schema.Boolean.pipe(Schema.withDecodingDefault(Effect.succeed(true))), @@ -580,6 +599,7 @@ export const ServerSettings = Schema.Struct({ sourceControlWriterModelSelection: Schema.NullOr(ModelSelection).pipe( Schema.withDecodingDefault(Effect.succeed(null)), ), + channelIntegrations: ChannelIntegrationSettings, // Legacy single-instance-per-driver settings. Continues to be the source // of truth until `providerInstances` (below) lands per-driver migration @@ -723,6 +743,22 @@ export const ServerSettingsPatch = Schema.Struct({ }), ), sourceControlWriterModelSelection: Schema.optionalKey(Schema.NullOr(ModelSelection)), + channelIntegrations: Schema.optionalKey( + Schema.Struct({ + discord: Schema.optionalKey( + Schema.Struct({ + enabled: Schema.optionalKey(Schema.Boolean), + projectId: Schema.optionalKey(Schema.NullOr(ProjectId)), + baseBranch: Schema.optionalKey(TrimmedNonEmptyString), + branchPrefix: Schema.optionalKey(TrimmedNonEmptyString), + applicationId: Schema.optionalKey(TrimmedString), + guildId: Schema.optionalKey(TrimmedString), + botToken: Schema.optionalKey(TrimmedString), + botTokenRedacted: Schema.optionalKey(Schema.Boolean), + }), + ), + }), + ), observability: Schema.optionalKey( Schema.Struct({ otlpTracesUrl: Schema.optionalKey(TrimmedString), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f7c6f4cc1f5..f056312ad87 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -104,7 +104,7 @@ importers: version: 7.0.0-dev.20260604.1 vite-plus: specifier: 'catalog:' - version: 0.2.2(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 0.2.2(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) apps/desktop: dependencies: @@ -168,7 +168,7 @@ importers: version: 4.3.0 vite-plus: specifier: 'catalog:' - version: 0.2.2(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 0.2.2(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) apps/marketing: dependencies: @@ -443,7 +443,16 @@ importers: dependencies: '@anthropic-ai/claude-agent-sdk': specifier: ^0.3.170 - version: 0.3.170(@anthropic-ai/sdk@0.93.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3) + version: 0.3.170(@anthropic-ai/sdk@0.93.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(zod@4.4.3) + '@copilotkit/channels-core': + specifier: 0.7.3 + version: 0.7.3(vitest@4.1.9)(zod@4.4.3) + '@copilotkit/channels-discord': + specifier: 0.7.3 + version: 0.7.3(@ag-ui/core@0.0.57)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.9) + '@copilotkit/channels-ui': + specifier: 0.7.3 + version: 0.7.3(@ag-ui/core@0.0.57) '@effect/platform-bun': specifier: 4.0.0-beta.103 version: 4.0.0-beta.103(bufferutil@4.1.0)(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9))(utf-8-validate@6.0.6) @@ -504,7 +513,7 @@ importers: version: link:../../packages/effect-codex-app-server vite-plus: specifier: 'catalog:' - version: 0.2.2(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 0.2.2(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) apps/web: dependencies: @@ -649,7 +658,7 @@ importers: version: '@voidzero-dev/vite-plus-core@0.2.2(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0)' vite-plus: specifier: 'catalog:' - version: 0.2.2(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 0.2.2(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) infra/relay: dependencies: @@ -676,10 +685,10 @@ importers: version: link:../../packages/shared alchemy: specifier: 2.0.0-beta.65 - version: 2.0.0-beta.65(a455401069e1fee89f31a277c51247f6) + version: 2.0.0-beta.65(cca023e78b21aab9077f3185bc54be18) drizzle-orm: specifier: 1.0.0-rc.4 - version: 1.0.0-rc.4(@cloudflare/workers-types@4.20260604.1)(@effect/sql-d1@4.0.0-beta.101(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9)))(@effect/sql-pg@4.0.0-beta.103(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9)))(@effect/sql-sqlite-bun@4.0.0-beta.103(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9)))(@libsql/client@0.17.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))(bun-types@1.3.14)(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9))(expo-sqlite@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(mysql2@3.22.4(@types/node@24.12.4))(pg@8.21.0)(zod@4.4.3) + version: 1.0.0-rc.4(@cloudflare/workers-types@4.20260604.1)(@effect/sql-d1@4.0.0-beta.101(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9)))(@effect/sql-pg@4.0.0-beta.103(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9)))(@effect/sql-sqlite-bun@4.0.0-beta.103(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9)))(@libsql/client@0.17.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@opentelemetry/api@1.9.1)(bun-types@1.3.14)(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9))(expo-sqlite@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(mysql2@3.22.4(@types/node@24.12.4))(pg@8.21.0)(zod@4.4.3) effect: specifier: 4.0.0-beta.103 version: 4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9) @@ -704,7 +713,7 @@ importers: version: '@voidzero-dev/vite-plus-core@0.2.2(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0)' vite-plus: specifier: 'catalog:' - version: 0.2.2(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 0.2.2(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) oxlint-plugin-t3code: dependencies: @@ -723,7 +732,7 @@ importers: version: 4.0.0-beta.103(patch_hash=a16b1e870d8c29e4a98b17cc4c638a4ff471753d8ca3487f78a22b759caf951b)(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9)) vite-plus: specifier: 'catalog:' - version: 0.2.2(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 0.2.2(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/client-runtime: dependencies: @@ -742,7 +751,7 @@ importers: version: 4.0.0-beta.103(patch_hash=a16b1e870d8c29e4a98b17cc4c638a4ff471753d8ca3487f78a22b759caf951b)(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9)) vite-plus: specifier: 'catalog:' - version: 0.2.2(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 0.2.2(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/contracts: dependencies: @@ -755,7 +764,7 @@ importers: version: 4.0.0-beta.103(patch_hash=a16b1e870d8c29e4a98b17cc4c638a4ff471753d8ca3487f78a22b759caf951b)(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9)) vite-plus: specifier: 'catalog:' - version: 0.2.2(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 0.2.2(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/effect-acp: dependencies: @@ -777,7 +786,7 @@ importers: version: 24.12.4 vite-plus: specifier: 'catalog:' - version: 0.2.2(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 0.2.2(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/effect-codex-app-server: dependencies: @@ -799,7 +808,7 @@ importers: version: 24.12.4 vite-plus: specifier: 'catalog:' - version: 0.2.2(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 0.2.2(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/shared: dependencies: @@ -833,7 +842,7 @@ importers: version: 24.12.4 vite-plus: specifier: 'catalog:' - version: 0.2.2(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 0.2.2(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/ssh: dependencies: @@ -858,7 +867,7 @@ importers: version: 24.12.4 vite-plus: specifier: 'catalog:' - version: 0.2.2(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 0.2.2(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages/tailscale: dependencies: @@ -880,7 +889,7 @@ importers: version: 24.12.4 vite-plus: specifier: 'catalog:' - version: 0.2.2(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 0.2.2(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) scripts: dependencies: @@ -914,13 +923,25 @@ importers: version: 6.0.5 vite-plus: specifier: 'catalog:' - version: 0.2.2(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + version: 0.2.2(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) packages: '@adobe/css-tools@4.5.0': resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} + '@ag-ui/client@0.0.57': + resolution: {integrity: sha512-Xap2alG9Z0/j5kb3x4D7oTpe2sw1dfrC9rgJJr2NZu5vKcm8dzIPNd31mF2B4zS3BKqYIu245yxKPhEtT30MHw==} + + '@ag-ui/core@0.0.57': + resolution: {integrity: sha512-gho1OWjNE6E3Rl7ZEZ1wr2CEpUHjLFU0FqzCZZk439TicLu+BfLCMkMokB07bMGlRmbJ60hM6LW60iOVauCx+Q==} + + '@ag-ui/encoder@0.0.57': + resolution: {integrity: sha512-ifD9NctR4xyPDR58xF9GK1bj/S8oECFkTeDfuYD8tXdbcOstIJ2TOqU2zhiCKnw7Vw+zR9Qv3TbsM9E7Gi9X3Q==} + + '@ag-ui/proto@0.0.57': + resolution: {integrity: sha512-pPENOZt0P6ibH8sCTgq05wLYXi5t3P9B5r/1bWYehXjUxtyOdnukSlWM++SsCIwUXsQdm/b3aBgGjEeTF7RenA==} + '@alcalzone/ansi-tokenize@0.2.5': resolution: {integrity: sha512-3NX/MpTdroi0aKz134A6RC2Gb2iXVECN4QaAXnvCIxxIm3C3AVB1mkUe8NaaiyvOpDfsrqWhYtj+Q6a62RrTsw==} engines: {node: '>=18'} @@ -1657,6 +1678,9 @@ packages: cpu: [x64] os: [win32] + '@bufbuild/protobuf@2.13.0': + resolution: {integrity: sha512-acq7c49vxfm1ggJ95P70TX7ABDM0vxr1SYD3BB0o0jnBLB4OAqeHyKuN+cD3w80gXEDQ2zxHpR6CUeA+O/aU9g==} + '@callstack/liquid-glass@0.7.1': resolution: {integrity: sha512-N2rzs8g3kneI5G/98AZdVtjc6OUFBBFwPxVGVMoXUEI7QiioB4+aWwbImg5h8Z3Vb/T+fgLYWmmNhi27wHwE4g==} peerDependencies: @@ -1667,6 +1691,9 @@ packages: resolution: {integrity: sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ==} engines: {node: '>=18'} + '@cfworker/json-schema@4.1.1': + resolution: {integrity: sha512-gAmrUZSGtKc3AiBL71iNWxDsyUC5uMaKKGdvzYsBoTW/xi42JQHl7eKV2OYzCUqvc+D2RCcf7EXY2iCyFIk6og==} + '@clack/core@0.5.0': resolution: {integrity: sha512-p3y0FIOwaYRUPRcMO7+dlmLh8PSRcrjuTndsiA0WAFbWES0mLZlrjVoBRZ9DzkPFJZG6KGkJmoEAY0ZcVWTkow==} @@ -1833,6 +1860,60 @@ packages: '@cloudflare/workers-types@5.20260726.1': resolution: {integrity: sha512-fKgRSm3sDmOdak1LGWehS4vSPSj7/zeu0NfmE62VPjBMqWgODcOGljYvq6A75sL+7YfY3iGFGb0jVEDYq+hlmw==} + '@copilotkit/channels-core@0.7.3': + resolution: {integrity: sha512-tj/Zil4Es6Z7MkSKH9Ak2n0xd0gyJwkqkW6VwseczZvTnJkPjt4Oq1K5ZT6tHtoaZKe33OqSkq9HDgRRj9tZWA==} + peerDependencies: + vitest: ^4.0.0 + peerDependenciesMeta: + vitest: + optional: true + + '@copilotkit/channels-discord@0.7.3': + resolution: {integrity: sha512-IbzAHbHW841bKishaSAYSlS9wShY9fNh0Xv4Yu+0vWcxdnMTTY2aB3WY784X8fZXkwDGwt/eMCvxrSe0PfTwHg==} + + '@copilotkit/channels-ui@0.7.3': + resolution: {integrity: sha512-uwRmhU9PlNKUFc/XXrhCJdcNHuQXa3rPE7lmjlPKr0matSXCHjsCfhMmDtgrDgTf8CCiVubwImT8CSH3vY3Y1A==} + + '@copilotkit/core@1.66.2': + resolution: {integrity: sha512-XWxbs72EkmhdCUgK80rRNjt+k3yTmbe0qP82iiU2cV5IWNbKbSvC9jO9ZAsa3Wbilhf2XTD1PSWa4dwN3xulHg==} + engines: {node: '>=18'} + + '@copilotkit/license-verifier@0.5.0': + resolution: {integrity: sha512-vrwKtIpYwF0FT9ZoYASH8owa2cGV0dhDvJGaCRaRMStwDxpc6DRdydKkhx8cWZXyBRxEYcq/Vygv4JvevhQQdQ==} + + '@copilotkit/shared@1.66.2': + resolution: {integrity: sha512-FmLAVm1jvSflxiVulozRbdft5dFPqP5EHyTq7Fl9CVwrxQR2Z2/CO9plifwKBJRkZpxIBsgp05i8NR3u6xUqgA==} + peerDependencies: + '@ag-ui/core': '>=0.0.48' + + '@discordjs/builders@1.14.1': + resolution: {integrity: sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ==} + engines: {node: '>=16.11.0'} + + '@discordjs/collection@1.5.3': + resolution: {integrity: sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==} + engines: {node: '>=16.11.0'} + + '@discordjs/collection@2.1.1': + resolution: {integrity: sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==} + engines: {node: '>=18'} + + '@discordjs/formatters@0.6.2': + resolution: {integrity: sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==} + engines: {node: '>=16.11.0'} + + '@discordjs/rest@2.6.3': + resolution: {integrity: sha512-wvOylxNYJkwKjctS/Mn5GP1w9r3/rzyH+ThD1JlAca6zEdlHs8QWBBUQJpU5Q+W6DoIj/Ljh1IPlZs7hTU+UAg==} + engines: {node: '>=18'} + + '@discordjs/util@1.2.0': + resolution: {integrity: sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==} + engines: {node: '>=18'} + + '@discordjs/ws@1.2.3': + resolution: {integrity: sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==} + engines: {node: '>=16.11.0'} + '@distilled.cloud/aws@0.30.2': resolution: {integrity: sha512-Uw2yZf7PJ2ienrKG49HN1ajnke65mTtHBUrSb/3ykeZ/8cLaSgwVhPscHxSzGByY0TEtYcYKNNmfUVISgGQl9g==} peerDependencies: @@ -2095,12 +2176,18 @@ packages: '@emnapi/core@1.11.1': resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + '@emnapi/core@1.11.2': + resolution: {integrity: sha512-TC8MkTuZUtcTSiFeuC0ksCh9QIJ5+F21MvZ4Wn4ORfYaFJ/0dsiudv5tVkejgwZlwQ39jL9WWDe2lz8x0WglOA==} + '@emnapi/runtime@1.10.0': resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} '@emnapi/runtime@1.11.1': resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + '@emnapi/runtime@1.11.2': + resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==} + '@emnapi/wasi-threads@1.2.1': resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} @@ -3098,6 +3185,14 @@ packages: cpu: [x64] os: [win32] + '@lukeed/csprng@1.1.0': + resolution: {integrity: sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==} + engines: {node: '>=8'} + + '@lukeed/uuid@2.0.1': + resolution: {integrity: sha512-qC72D4+CDdjGqJvkFMMEAtancHUQ7/d/tAiHf64z8MopFDmcrtbcJuerDtFceuAfQJ2pDSfCKCtbqoGBNnwg0w==} + engines: {node: '>=8'} + '@malept/cross-spawn-promise@2.0.0': resolution: {integrity: sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==} engines: {node: '>= 12.13.0'} @@ -3268,6 +3363,10 @@ packages: '@opencode-ai/sdk@1.15.13': resolution: {integrity: sha512-4TwojIoQ8EG6/mVBuUVYZXiFcwNmiiytEnjnvyuvSJjGwFIlw2YIBFxtSVC3FbwwbwHT63teh1RHiQUUC4U5xw==} + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} @@ -3284,6 +3383,9 @@ packages: '@oxc-project/types@0.139.0': resolution: {integrity: sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==} + '@oxc-project/types@0.140.0': + resolution: {integrity: sha512-h5LUOzGArYemnW1NMz/DuuQhBi96J6JL2Bk8zE4kvqxB5Sg3jxmCiH4uyOWHDkiKSt5vWlG4FIwCR/DbstcNRQ==} + '@oxfmt/binding-android-arm-eabi@0.57.0': resolution: {integrity: sha512-qVBsEO+KugOsCmUHcO8iqNnqc65p7PCKpCs8M66mPZ+Ri+CWbcpoQOEJBg2OTu03+0qu++NK1jj6IzvQVs0Sig==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3618,6 +3720,10 @@ packages: '@preact/signals-core@1.14.2': resolution: {integrity: sha512-RZHdBj9ZF4n40Rp4jS052EHHjBWf96P9oNdXPfhQTovCuWY9iQn3Gq+gOTJSgBO9A/JBuPfMOWsSX/lIU9Pc/A==} + '@protobuf-ts/protoc@2.11.1': + resolution: {integrity: sha512-mUZJaV0daGO6HUX90o/atzQ6A7bbN2RSuHtdwo8SSF2Qoe3zHwa4IHyCN1evftTeHfLmdz+45qo47sL+5P8nyg==} + hasBin: true + '@radix-ui/primitive@1.1.3': resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} @@ -3985,6 +4091,12 @@ packages: cpu: [arm64] os: [android] + '@rolldown/binding-android-arm64@1.2.0': + resolution: {integrity: sha512-9yB1l95IrJuNGDFdOYe79vdApdz6WWBCObE+rQ2LUliYUlcyFwSYIb2xb5/Ifw7dAtMy2ZqNyd8QTSOc7duAKw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + '@rolldown/binding-darwin-arm64@1.0.0-rc.17': resolution: {integrity: sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3997,6 +4109,12 @@ packages: cpu: [arm64] os: [darwin] + '@rolldown/binding-darwin-arm64@1.2.0': + resolution: {integrity: sha512-pexNaW9ACLUOaBITOpU6qVu4VrsOFIjTv6bzgu0YUATo4eUJx0V605PxwZfndpPOn0ilqGqvGQ0M8UW0IE24jg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + '@rolldown/binding-darwin-x64@1.0.0-rc.17': resolution: {integrity: sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4009,6 +4127,12 @@ packages: cpu: [x64] os: [darwin] + '@rolldown/binding-darwin-x64@1.2.0': + resolution: {integrity: sha512-NqKYaq0355ZmNMG4QGpxtEDxsc7tGDhjhCm4PpE0cwnBW+5Il95LJyq414niEiaKLVjnVHBEjSo1wngKxJNiFw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + '@rolldown/binding-freebsd-x64@1.0.0-rc.17': resolution: {integrity: sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4021,6 +4145,12 @@ packages: cpu: [x64] os: [freebsd] + '@rolldown/binding-freebsd-x64@1.2.0': + resolution: {integrity: sha512-3vPoHzh6eBTz9IbB0/qZdSr0Qeks2echn+I4cHu2joV74VriPDdldswksEDzrl1mBB+oPRi+67+3Ib59paxIPQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': resolution: {integrity: sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4033,6 +4163,12 @@ packages: cpu: [arm] os: [linux] + '@rolldown/binding-linux-arm-gnueabihf@1.2.0': + resolution: {integrity: sha512-E6NNefZ1bUVmKJq2tJkf45J4Zyczj7qm9rUT7NY+Xo2474Y13qWAwc2tvBt0BAVbmtXR1llkxXg0Ou1jbDf2SQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': resolution: {integrity: sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4047,6 +4183,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-arm64-gnu@1.2.0': + resolution: {integrity: sha512-D+TgkdgM1vu+7/Fpf8+v0ARW+RXEP9Ccazgm8zQ4JFFd9Q7SrYQ2TakU5S5ihazQDgpKyAgZDOcIFsvoHmTZ8w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': resolution: {integrity: sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4061,6 +4204,13 @@ packages: os: [linux] libc: [musl] + '@rolldown/binding-linux-arm64-musl@1.2.0': + resolution: {integrity: sha512-wUqdwJBbAv0APN87GecstdMUtLjjNTs0hBALpxETD73mccFxdmt/XeizXDtN5RAlBwNKmI+Tg+blect2G+8IeQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': resolution: {integrity: sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4075,6 +4225,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-ppc64-gnu@1.2.0': + resolution: {integrity: sha512-9DtF35qR9/NrfhM4oxLplCzVVjE+KKm8Pjemi0i/sdhAWkUasjmSo8WTTubNJClhSHCfyk2yeyoXDQEDPtDAAw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': resolution: {integrity: sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4089,6 +4246,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-s390x-gnu@1.2.0': + resolution: {integrity: sha512-RzuHrBh8X8Hntd2N4VR02QGEciq/9JhcZoTpR/Cee6otRrlILGCf3cg2ygHuih+ZebUnWmMrDX6ITI85btO6rQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': resolution: {integrity: sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4103,6 +4267,13 @@ packages: os: [linux] libc: [glibc] + '@rolldown/binding-linux-x64-gnu@1.2.0': + resolution: {integrity: sha512-MK7L0018jjh1jR3mh21G2j1zAVcpscJBlPo2z19pRjv2XOYGRhaV4LyiD8HO6nCDdZln9IFgCMIV5yt4E3klGQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': resolution: {integrity: sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4117,6 +4288,13 @@ packages: os: [linux] libc: [musl] + '@rolldown/binding-linux-x64-musl@1.2.0': + resolution: {integrity: sha512-gyrxLQ9NfGb/9LoVnC4kb9miUghw1mghnkfYvNHSnVIXriabnfgGPUP4RLcJm87q3KgYz4FYUG8IDiWUT+CpSw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': resolution: {integrity: sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4129,6 +4307,12 @@ packages: cpu: [arm64] os: [openharmony] + '@rolldown/binding-openharmony-arm64@1.2.0': + resolution: {integrity: sha512-/6VFMQGRmrhP77KXDC+StIxGzcNp5JOIyYtw0CQ8gPlzhpiIRucYfoM5FaFamHd5BJYIdH86yfP46l1p3WdrFA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': resolution: {integrity: sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4139,6 +4323,11 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} cpu: [wasm32] + '@rolldown/binding-wasm32-wasi@1.2.0': + resolution: {integrity: sha512-rwdbUL465kisF24WEJLvP3JrEG6E5GRuIHt5wpMwHGERtHe4Wm2CIvtf5gTBgr2tGOHKh5NdKEAFS2VkOPE91g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': resolution: {integrity: sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4151,6 +4340,12 @@ packages: cpu: [arm64] os: [win32] + '@rolldown/binding-win32-arm64-msvc@1.2.0': + resolution: {integrity: sha512-+5suHwRiKGmhwyUaNT8a5QbrBvLFh2DbO910TEmGRH1aSxwrCezodvGQnulv4uiWEIv1Kq4ypRsJ5+O+ry1DiA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': resolution: {integrity: sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -4163,6 +4358,12 @@ packages: cpu: [x64] os: [win32] + '@rolldown/binding-win32-x64-msvc@1.2.0': + resolution: {integrity: sha512-WfFv6/qGufotqBSBzBYwgpCkJBk8Nj7697LL9vTz/XWc67e0r3oewu8iMRwQj3AUL45GVD7wVsPjCsAAtW66Wg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + '@rolldown/plugin-babel@0.2.3': resolution: {integrity: sha512-+zEk16yGlz1F9STiRr6uG9hmIXb6nprjLczV/htGptYuLoCuxb+itZ03RKCEeOhBpDDd1NU7qF6x1VLMUp62bw==} engines: {node: '>=22.12.0 || ^24.0.0'} @@ -4333,6 +4534,28 @@ packages: cpu: [x64] os: [win32] + '@sapphire/async-queue@1.5.5': + resolution: {integrity: sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + + '@sapphire/shapeshift@4.0.0': + resolution: {integrity: sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==} + engines: {node: '>=v16'} + + '@sapphire/snowflake@3.5.5': + resolution: {integrity: sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + + '@segment/analytics-core@1.8.2': + resolution: {integrity: sha512-5FDy6l8chpzUfJcNlIcyqYQq4+JTUynlVoCeCUuVz+l+6W0PXg+ljKp34R4yLVCcY5VVZohuW+HH0VLWdwYVAg==} + + '@segment/analytics-generic-utils@1.2.0': + resolution: {integrity: sha512-DfnW6mW3YQOLlDQQdR89k4EqfHb0g/3XvBXkovH1FstUN93eL1kfW9CsDcVQyH3bAC5ZsFyjA/o/1Q2j0QeoWw==} + + '@segment/analytics-node@2.3.0': + resolution: {integrity: sha512-fOXLL8uY0uAWw/sTLmezze80hj8YGgXXlAfvSS6TUmivk4D/SP0C0sxnbpFdkUzWg2zT64qWIZj26afEtSnxUA==} + engines: {node: '>=20'} + '@shikijs/core@4.2.0': resolution: {integrity: sha512-Hc87Ab1Ld/vEbZRCbwx344I5v+4RU8CVToUTRkqXL1+TjbuOp9U5Xa0M23V4GEWHxVn+yO5otb+HkQVm3ptWQQ==} engines: {node: '>=20'} @@ -4670,6 +4893,10 @@ packages: resolution: {integrity: sha512-qhCRSFei0hokQr3xYcQXqxsRD/LKlgHCxHXtKHrQoImp4x2Zu6tUOpUGVH4y2qexIrzSu3aibQBNNfC3Eay6Mg==} engines: {node: '>=18'} + '@tanstack/pacer@0.20.1': + resolution: {integrity: sha512-ZNQ1bIL6eUXVKdic0tiImvBVkWrg/IoSK6VIacTrO3d3HAGnd70qFJNJagR/YOJIOw4EKGWnodwpYZkN1pWuVQ==} + engines: {node: '>=18'} + '@tanstack/query-core@5.100.14': resolution: {integrity: sha512-5X41dGpxgeaHISCRW2oYwcSycZeULZzAunaudXT9ov1KOTj9xwt0CH6hbwqP1/z74ZWF7rYFnDpyYH07XFcZew==} @@ -4866,6 +5093,9 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@types/uuid@10.0.0': + resolution: {integrity: sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ==} + '@types/webidl-conversions@7.0.3': resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==} @@ -4993,6 +5223,10 @@ packages: '@vitest/utils@4.1.9': resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==} + '@vladfrangu/async_event_emitter@2.4.7': + resolution: {integrity: sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==} + engines: {node: '>=v14.0.0', npm: '>=7.0.0'} + '@voidzero-dev/vite-plus-core@0.2.2': resolution: {integrity: sha512-yAbKexF3npOGjg1N5EtXxun+7vdM/0x6QE5jucO/dv0LFhCAIzSN3UvLVCeamJt/Bz3jt7DLqQHEgXXrjy8drA==} engines: {node: ^20.19.0 || ^22.18.0 || >=24.11.0} @@ -5613,6 +5847,9 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + bufferutil@4.1.0: resolution: {integrity: sha512-ZMANVnAixE6AWWnPzlW2KpUrxhm9woycYvPOo67jWHyFowASTEd9s+QN1EIMsSDtwhIxN4sWE1jotpuDUIgyIw==} engines: {node: '>=6.14.2'} @@ -5861,6 +6098,9 @@ packages: resolution: {integrity: sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==} engines: {node: '>=0.10.0'} + compare-versions@6.1.1: + resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} + compressible@2.0.18: resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==} engines: {node: '>= 0.6'} @@ -6097,6 +6337,13 @@ packages: dir-compare@4.2.0: resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==} + discord-api-types@0.38.52: + resolution: {integrity: sha512-uwe9EKfbjsmgWc2fdFjvDbj+dQqx3lp7wqDCmIha0jInuU+xeQjkCK9tMMn+p7RXfdVQORCInq4cD3U2ymDmyg==} + + discord.js@14.27.0: + resolution: {integrity: sha512-qHbFlFG2N7y3LjPySYsL6A1+BnX6bkTVgo842EX0CqVPk/KTMwZkojPHEXKsQUpWZNyz5BISNHK1cPpQw0+m4A==} + engines: {node: '>=18'} + dmg-builder@26.15.6: resolution: {integrity: sha512-nr5vQxEhM0REomp1qiHbc6V99yrfBZy+wUU56VXADfSOlLj8PdLqsHiRe7b+FbqKesiyv4ax+k1GVwGonYKuCg==} @@ -6824,6 +7071,9 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} + fast-json-patch@3.1.1: + resolution: {integrity: sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==} + fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} @@ -6932,8 +7182,8 @@ packages: resolution: {integrity: sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==} engines: {node: '>=20'} - form-data@4.0.5: - resolution: {integrity: sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==} + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} engines: {node: '>= 6'} forwarded@0.2.0: @@ -7199,6 +7449,9 @@ packages: idb-keyval@6.2.1: resolution: {integrity: sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==} + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + ignore@5.3.2: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} @@ -7422,6 +7675,9 @@ packages: resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true + jose@5.10.0: + resolution: {integrity: sha512-s+3Al/p9g32Iq+oqXxkW//7jk2Vig6FF1CFqzVXoTUXt2qz89YWbL+OwS17NFYEvxC35n0FKeGO2LGYSxeM2Gg==} + jose@6.2.2: resolution: {integrity: sha512-d7kPDd34KO/YnzaDOlikGpOurfF0ByC2sEV4cANCtdqLlTfBlw2p14O/5d/zv40gJPbIQxfES3nSx1/oYNyuZQ==} @@ -7560,6 +7816,12 @@ packages: cpu: [arm64] os: [android] + lightningcss-android-arm64@1.33.0: + resolution: {integrity: sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + lightningcss-darwin-arm64@1.30.1: resolution: {integrity: sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==} engines: {node: '>= 12.0.0'} @@ -7578,6 +7840,12 @@ packages: cpu: [arm64] os: [darwin] + lightningcss-darwin-arm64@1.33.0: + resolution: {integrity: sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + lightningcss-darwin-x64@1.30.1: resolution: {integrity: sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==} engines: {node: '>= 12.0.0'} @@ -7596,6 +7864,12 @@ packages: cpu: [x64] os: [darwin] + lightningcss-darwin-x64@1.33.0: + resolution: {integrity: sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + lightningcss-freebsd-x64@1.30.1: resolution: {integrity: sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==} engines: {node: '>= 12.0.0'} @@ -7614,6 +7888,12 @@ packages: cpu: [x64] os: [freebsd] + lightningcss-freebsd-x64@1.33.0: + resolution: {integrity: sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + lightningcss-linux-arm-gnueabihf@1.30.1: resolution: {integrity: sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==} engines: {node: '>= 12.0.0'} @@ -7632,6 +7912,12 @@ packages: cpu: [arm] os: [linux] + lightningcss-linux-arm-gnueabihf@1.33.0: + resolution: {integrity: sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + lightningcss-linux-arm64-gnu@1.30.1: resolution: {integrity: sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==} engines: {node: '>= 12.0.0'} @@ -7653,6 +7939,13 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-arm64-gnu@1.33.0: + resolution: {integrity: sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + lightningcss-linux-arm64-musl@1.30.1: resolution: {integrity: sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==} engines: {node: '>= 12.0.0'} @@ -7674,6 +7967,13 @@ packages: os: [linux] libc: [musl] + lightningcss-linux-arm64-musl@1.33.0: + resolution: {integrity: sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + lightningcss-linux-x64-gnu@1.30.1: resolution: {integrity: sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==} engines: {node: '>= 12.0.0'} @@ -7695,6 +7995,13 @@ packages: os: [linux] libc: [glibc] + lightningcss-linux-x64-gnu@1.33.0: + resolution: {integrity: sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + lightningcss-linux-x64-musl@1.30.1: resolution: {integrity: sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==} engines: {node: '>= 12.0.0'} @@ -7716,6 +8023,13 @@ packages: os: [linux] libc: [musl] + lightningcss-linux-x64-musl@1.33.0: + resolution: {integrity: sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + lightningcss-win32-arm64-msvc@1.30.1: resolution: {integrity: sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==} engines: {node: '>= 12.0.0'} @@ -7734,6 +8048,12 @@ packages: cpu: [arm64] os: [win32] + lightningcss-win32-arm64-msvc@1.33.0: + resolution: {integrity: sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + lightningcss-win32-x64-msvc@1.30.1: resolution: {integrity: sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==} engines: {node: '>= 12.0.0'} @@ -7752,6 +8072,12 @@ packages: cpu: [x64] os: [win32] + lightningcss-win32-x64-msvc@1.33.0: + resolution: {integrity: sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + lightningcss@1.30.1: resolution: {integrity: sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==} engines: {node: '>= 12.0.0'} @@ -7764,6 +8090,10 @@ packages: resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} engines: {node: '>= 12.0.0'} + lightningcss@1.33.0: + resolution: {integrity: sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==} + engines: {node: '>= 12.0.0'} + locate-path@3.0.0: resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} engines: {node: '>=6'} @@ -7778,6 +8108,9 @@ packages: resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + lodash.snakecase@4.1.1: + resolution: {integrity: sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==} + lodash.throttle@4.1.1: resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} @@ -7836,6 +8169,9 @@ packages: resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true + magic-bytes.js@1.13.1: + resolution: {integrity: sha512-x5sn4UX2k5gCWlcfmoFwG4TPie8+dctESyqOBdhB5p6MsgWXdBKGmt9nXPObj/JI50TTL928lc5Yt1WntMn1bw==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -8265,6 +8601,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@3.3.17: + resolution: {integrity: sha512-xQLf0A3HOMlgHq0n247/LRuAOYmB7dXJ/DvAxGvsSBij45XtBSmQycu+F8ODbHwns/XyFZagyL1+J0Offw1E0g==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + negotiator@0.6.3: resolution: {integrity: sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==} engines: {node: '>= 0.6'} @@ -8544,6 +8885,9 @@ packages: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} + partial-json@0.1.7: + resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==} + patch-console@2.0.0: resolution: {integrity: sha512-0YNdUceMdaQwoKce1gatDScmMo5pu/tfABfnzEqeG0gtTmd7mh/WcwgUjtAeOU7N8nFFlbQBnFK2gXW5fGvmMA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -8652,6 +8996,9 @@ packages: pgpass@1.0.5: resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + phoenix@1.8.9: + resolution: {integrity: sha512-/2qzAZB3P2s08fFAYaG65lqaNFmVXUSlXdY4/JDdDKIC81y2cFWkPwI8gycy4VLpv197JwZ5PpBf3VhoG32yGA==} + piccolore@0.1.3: resolution: {integrity: sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==} @@ -8666,6 +9013,10 @@ packages: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + picomatch@4.0.5: + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} + pkce-challenge@5.0.1: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} engines: {node: '>=16.20.0'} @@ -8703,6 +9054,10 @@ packages: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} + postcss@8.5.25: + resolution: {integrity: sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==} + engines: {node: ^10 || ^12 || >=14} + postgres-array@2.0.0: resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} engines: {node: '>=4'} @@ -9270,6 +9625,11 @@ packages: engines: {node: ^20.19.0 || >=22.12.0} hasBin: true + rolldown@1.2.0: + resolution: {integrity: sha512-u7tgm5l4Yw1iTqUL4EcYOAt7fFvCgQMLeidrnD4GALlC6aOznCjezYajgxeyKw27u0Q5N7fwgCzjVyPIWzwuBA==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + rollup@4.61.0: resolution: {integrity: sha512-T9mWdbWfQtp0B5lv/HX+wrhYsmXRlcWnXXmJbXqKJhlRaoS6KMhq0gpyzW4UJfclcxrEdLnTgjT2NjruLONu0g==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -9282,6 +9642,9 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rxjs@7.8.1: + resolution: {integrity: sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==} + safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} @@ -9785,6 +10148,9 @@ packages: ts-algebra@2.0.0: resolution: {integrity: sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==} + ts-mixer@6.0.4: + resolution: {integrity: sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==} + tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} @@ -9835,8 +10201,8 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - undici@6.26.0: - resolution: {integrity: sha512-4yqz8a3n5HmGTlsbADNtr/dJlhkh/55Rq798G6ibiULcXbDtaLpTl1pvdqcbFfeoj3iSi52lePFM7h9H21cw/A==} + undici@6.28.0: + resolution: {integrity: sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==} engines: {node: '>=18.17'} undici@7.27.1: @@ -10000,6 +10366,9 @@ packages: until-async@3.0.2: resolution: {integrity: sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw==} + untruncate-json@0.0.1: + resolution: {integrity: sha512-4W9enDK4X1y1s2S/Rz7ysw6kDuMS3VmRjMFg7GZrNO+98OSe+x5Lh7PKYoVjy3lW/1wmhs6HW0lusnQRHgMarA==} + unzipper@0.12.5: resolution: {integrity: sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==} @@ -10056,6 +10425,10 @@ packages: resolution: {integrity: sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==} engines: {node: '>= 0.4.0'} + uuid@11.1.1: + resolution: {integrity: sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==} + hasBin: true + uuid@14.0.1: resolution: {integrity: sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==} hasBin: true @@ -10101,6 +10474,49 @@ packages: '@vitest/browser-webdriverio': optional: true + vite@8.2.0: + resolution: {integrity: sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': 24.12.4 + '@vitejs/devtools': ^0.4.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.9.0 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + vitefu@1.1.3: resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} peerDependencies: @@ -10485,6 +10901,34 @@ snapshots: '@adobe/css-tools@4.5.0': optional: true + '@ag-ui/client@0.0.57': + dependencies: + '@ag-ui/core': 0.0.57 + '@ag-ui/encoder': 0.0.57 + '@ag-ui/proto': 0.0.57 + '@types/uuid': 10.0.0 + compare-versions: 6.1.1 + fast-json-patch: 3.1.1 + rxjs: 7.8.1 + untruncate-json: 0.0.1 + uuid: 11.1.1 + zod: 3.25.76 + + '@ag-ui/core@0.0.57': + dependencies: + zod: 3.25.76 + + '@ag-ui/encoder@0.0.57': + dependencies: + '@ag-ui/core': 0.0.57 + '@ag-ui/proto': 0.0.57 + + '@ag-ui/proto@0.0.57': + dependencies: + '@ag-ui/core': 0.0.57 + '@bufbuild/protobuf': 2.13.0 + '@protobuf-ts/protoc': 2.11.1 + '@alcalzone/ansi-tokenize@0.2.5': dependencies: ansi-styles: 6.2.3 @@ -10516,10 +10960,10 @@ snapshots: '@anthropic-ai/claude-agent-sdk-win32-x64@0.3.170': optional: true - '@anthropic-ai/claude-agent-sdk@0.3.170(@anthropic-ai/sdk@0.93.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(zod@4.4.3)': + '@anthropic-ai/claude-agent-sdk@0.3.170(@anthropic-ai/sdk@0.93.0(zod@4.4.3))(@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3))(zod@4.4.3)': dependencies: '@anthropic-ai/sdk': 0.93.0(zod@4.4.3) - '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) + '@modelcontextprotocol/sdk': 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3) zod: 4.4.3 optionalDependencies: '@anthropic-ai/claude-agent-sdk-darwin-arm64': 0.3.170 @@ -10609,7 +11053,7 @@ snapshots: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 js-yaml: 4.2.0 - picomatch: 4.0.4 + picomatch: 4.0.5 retext-smartypants: 6.2.0 shiki: 4.2.0 smol-toml: 1.7.0 @@ -11451,6 +11895,8 @@ snapshots: '@bruits/satteri-win32-x64-msvc@0.9.3': optional: true + '@bufbuild/protobuf@2.13.0': {} + '@callstack/liquid-glass@0.7.1(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3)': dependencies: react: 19.2.3 @@ -11460,6 +11906,9 @@ snapshots: dependencies: fontkitten: 1.0.3 + '@cfworker/json-schema@4.1.1': + optional: true + '@clack/core@0.5.0': dependencies: picocolors: 1.1.1 @@ -11640,18 +12089,160 @@ snapshots: '@cloudflare/workers-types@5.20260726.1': {} - '@distilled.cloud/aws@0.30.2(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9))': + '@copilotkit/channels-core@0.7.3(vitest@4.1.9)(zod@3.25.76)': dependencies: - '@aws-crypto/crc32': 5.2.0 - '@aws-crypto/util': 5.2.0 - '@aws-sdk/credential-providers': 3.1062.0 - '@aws-sdk/types': 3.973.10 - '@distilled.cloud/core': 0.30.2(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9)) - '@smithy/shared-ini-file-loader': 4.5.6 - '@smithy/types': 4.14.3 - '@smithy/util-base64': 4.4.6 - aws4fetch: 1.0.20 - effect: 4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9) + '@ag-ui/client': 0.0.57 + '@ag-ui/core': 0.0.57 + '@copilotkit/channels-ui': 0.7.3(@ag-ui/core@0.0.57) + '@copilotkit/core': 1.66.2(@ag-ui/core@0.0.57)(zod@3.25.76) + '@copilotkit/shared': 1.66.2(@ag-ui/core@0.0.57) + zod-to-json-schema: 3.25.2(zod@3.25.76) + optionalDependencies: + vitest: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(@vitest/browser-preview@4.1.9)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(vite@8.2.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)) + transitivePeerDependencies: + - encoding + - zod + + '@copilotkit/channels-core@0.7.3(vitest@4.1.9)(zod@4.4.3)': + dependencies: + '@ag-ui/client': 0.0.57 + '@ag-ui/core': 0.0.57 + '@copilotkit/channels-ui': 0.7.3(@ag-ui/core@0.0.57) + '@copilotkit/core': 1.66.2(@ag-ui/core@0.0.57)(zod@4.4.3) + '@copilotkit/shared': 1.66.2(@ag-ui/core@0.0.57) + zod-to-json-schema: 3.25.2(zod@4.4.3) + optionalDependencies: + vitest: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(@vitest/browser-preview@4.1.9)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(vite@8.2.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)) + transitivePeerDependencies: + - encoding + - zod + + '@copilotkit/channels-discord@0.7.3(@ag-ui/core@0.0.57)(bufferutil@4.1.0)(utf-8-validate@6.0.6)(vitest@4.1.9)': + dependencies: + '@ag-ui/client': 0.0.57 + '@copilotkit/channels-core': 0.7.3(vitest@4.1.9)(zod@3.25.76) + '@copilotkit/channels-ui': 0.7.3(@ag-ui/core@0.0.57) + discord.js: 14.27.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + zod: 3.25.76 + transitivePeerDependencies: + - '@ag-ui/core' + - bufferutil + - encoding + - utf-8-validate + - vitest + + '@copilotkit/channels-ui@0.7.3(@ag-ui/core@0.0.57)': + dependencies: + '@copilotkit/shared': 1.66.2(@ag-ui/core@0.0.57) + transitivePeerDependencies: + - '@ag-ui/core' + - encoding + + '@copilotkit/core@1.66.2(@ag-ui/core@0.0.57)(zod@3.25.76)': + dependencies: + '@ag-ui/client': 0.0.57 + '@copilotkit/shared': 1.66.2(@ag-ui/core@0.0.57) + '@tanstack/pacer': 0.20.1 + phoenix: 1.8.9 + rxjs: 7.8.1 + zod-to-json-schema: 3.25.2(zod@3.25.76) + transitivePeerDependencies: + - '@ag-ui/core' + - encoding + - zod + + '@copilotkit/core@1.66.2(@ag-ui/core@0.0.57)(zod@4.4.3)': + dependencies: + '@ag-ui/client': 0.0.57 + '@copilotkit/shared': 1.66.2(@ag-ui/core@0.0.57) + '@tanstack/pacer': 0.20.1 + phoenix: 1.8.9 + rxjs: 7.8.1 + zod-to-json-schema: 3.25.2(zod@4.4.3) + transitivePeerDependencies: + - '@ag-ui/core' + - encoding + - zod + + '@copilotkit/license-verifier@0.5.0': {} + + '@copilotkit/shared@1.66.2(@ag-ui/core@0.0.57)': + dependencies: + '@ag-ui/client': 0.0.57 + '@ag-ui/core': 0.0.57 + '@copilotkit/license-verifier': 0.5.0 + '@segment/analytics-node': 2.3.0 + '@standard-schema/spec': 1.1.0 + chalk: 4.1.2 + graphql: 16.14.1 + partial-json: 0.1.7 + uuid: 11.1.1 + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + transitivePeerDependencies: + - encoding + + '@discordjs/builders@1.14.1': + dependencies: + '@discordjs/formatters': 0.6.2 + '@discordjs/util': 1.2.0 + '@sapphire/shapeshift': 4.0.0 + discord-api-types: 0.38.52 + fast-deep-equal: 3.1.3 + ts-mixer: 6.0.4 + tslib: 2.8.1 + + '@discordjs/collection@1.5.3': {} + + '@discordjs/collection@2.1.1': {} + + '@discordjs/formatters@0.6.2': + dependencies: + discord-api-types: 0.38.52 + + '@discordjs/rest@2.6.3': + dependencies: + '@discordjs/collection': 2.1.1 + '@discordjs/util': 1.2.0 + '@sapphire/async-queue': 1.5.5 + '@sapphire/snowflake': 3.5.5 + '@vladfrangu/async_event_emitter': 2.4.7 + discord-api-types: 0.38.52 + magic-bytes.js: 1.13.1 + tslib: 2.8.1 + undici: 6.28.0 + + '@discordjs/util@1.2.0': + dependencies: + discord-api-types: 0.38.52 + + '@discordjs/ws@1.2.3(bufferutil@4.1.0)(utf-8-validate@6.0.6)': + dependencies: + '@discordjs/collection': 2.1.1 + '@discordjs/rest': 2.6.3 + '@discordjs/util': 1.2.0 + '@sapphire/async-queue': 1.5.5 + '@types/ws': 8.18.1 + '@vladfrangu/async_event_emitter': 2.4.7 + discord-api-types: 0.38.52 + tslib: 2.8.1 + ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@distilled.cloud/aws@0.30.2(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9))': + dependencies: + '@aws-crypto/crc32': 5.2.0 + '@aws-crypto/util': 5.2.0 + '@aws-sdk/credential-providers': 3.1062.0 + '@aws-sdk/types': 3.973.10 + '@distilled.cloud/core': 0.30.2(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9)) + '@smithy/shared-ini-file-loader': 4.5.6 + '@smithy/types': 4.14.3 + '@smithy/util-base64': 4.4.6 + aws4fetch: 1.0.20 + effect: 4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9) fast-xml-parser: 5.8.0 '@distilled.cloud/axiom@0.30.2(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9))': @@ -11982,6 +12573,12 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/core@1.11.2': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + '@emnapi/runtime@1.10.0': dependencies: tslib: 2.8.1 @@ -11992,6 +12589,11 @@ snapshots: tslib: 2.8.1 optional: true + '@emnapi/runtime@1.11.2': + dependencies: + tslib: 2.8.1 + optional: true + '@emnapi/wasi-threads@1.2.1': dependencies: tslib: 2.8.1 @@ -13193,6 +13795,12 @@ snapshots: '@libsql/win32-x64-msvc@0.5.29': optional: true + '@lukeed/csprng@1.1.0': {} + + '@lukeed/uuid@2.0.1': + dependencies: + '@lukeed/csprng': 1.1.0 + '@malept/cross-spawn-promise@2.0.0': dependencies: cross-spawn: 7.0.6 @@ -13206,7 +13814,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': + '@modelcontextprotocol/sdk@1.29.0(@cfworker/json-schema@4.1.1)(zod@4.4.3)': dependencies: '@hono/node-server': 1.19.14(hono@4.12.27) ajv: 8.20.0 @@ -13225,6 +13833,8 @@ snapshots: raw-body: 3.0.2 zod: 4.4.3 zod-to-json-schema: 3.25.2(zod@4.4.3) + optionalDependencies: + '@cfworker/json-schema': 4.1.1 transitivePeerDependencies: - supports-color @@ -13273,6 +13883,13 @@ snapshots: '@tybys/wasm-util': 0.10.3 optional: true + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2)': + dependencies: + '@emnapi/core': 1.11.2 + '@emnapi/runtime': 1.11.2 + '@tybys/wasm-util': 0.10.3 + optional: true + '@neon-rs/load@0.0.4': {} '@noble/curves@1.9.1': @@ -13385,6 +14002,9 @@ snapshots: dependencies: cross-spawn: 7.0.6 + '@opentelemetry/api@1.9.1': + optional: true + '@oslojs/encoding@1.1.0': {} '@oxc-project/runtime@0.138.0': {} @@ -13396,6 +14016,9 @@ snapshots: '@oxc-project/types@0.139.0': {} + '@oxc-project/types@0.140.0': + optional: true + '@oxfmt/binding-android-arm-eabi@0.57.0': optional: true @@ -13616,6 +14239,8 @@ snapshots: '@preact/signals-core@1.14.2': {} + '@protobuf-ts/protoc@2.11.1': {} + '@radix-ui/primitive@1.1.3': {} '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.16))(@types/react@19.2.16)(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': @@ -14278,72 +14903,108 @@ snapshots: '@rolldown/binding-android-arm64@1.1.5': optional: true + '@rolldown/binding-android-arm64@1.2.0': + optional: true + '@rolldown/binding-darwin-arm64@1.0.0-rc.17': optional: true '@rolldown/binding-darwin-arm64@1.1.5': optional: true + '@rolldown/binding-darwin-arm64@1.2.0': + optional: true + '@rolldown/binding-darwin-x64@1.0.0-rc.17': optional: true '@rolldown/binding-darwin-x64@1.1.5': optional: true + '@rolldown/binding-darwin-x64@1.2.0': + optional: true + '@rolldown/binding-freebsd-x64@1.0.0-rc.17': optional: true '@rolldown/binding-freebsd-x64@1.1.5': optional: true + '@rolldown/binding-freebsd-x64@1.2.0': + optional: true + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.17': optional: true '@rolldown/binding-linux-arm-gnueabihf@1.1.5': optional: true + '@rolldown/binding-linux-arm-gnueabihf@1.2.0': + optional: true + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.17': optional: true '@rolldown/binding-linux-arm64-gnu@1.1.5': optional: true + '@rolldown/binding-linux-arm64-gnu@1.2.0': + optional: true + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.17': optional: true '@rolldown/binding-linux-arm64-musl@1.1.5': optional: true + '@rolldown/binding-linux-arm64-musl@1.2.0': + optional: true + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.17': optional: true '@rolldown/binding-linux-ppc64-gnu@1.1.5': optional: true + '@rolldown/binding-linux-ppc64-gnu@1.2.0': + optional: true + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.17': optional: true '@rolldown/binding-linux-s390x-gnu@1.1.5': optional: true + '@rolldown/binding-linux-s390x-gnu@1.2.0': + optional: true + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.17': optional: true '@rolldown/binding-linux-x64-gnu@1.1.5': optional: true + '@rolldown/binding-linux-x64-gnu@1.2.0': + optional: true + '@rolldown/binding-linux-x64-musl@1.0.0-rc.17': optional: true '@rolldown/binding-linux-x64-musl@1.1.5': optional: true + '@rolldown/binding-linux-x64-musl@1.2.0': + optional: true + '@rolldown/binding-openharmony-arm64@1.0.0-rc.17': optional: true '@rolldown/binding-openharmony-arm64@1.1.5': optional: true + '@rolldown/binding-openharmony-arm64@1.2.0': + optional: true + '@rolldown/binding-wasm32-wasi@1.0.0-rc.17': dependencies: '@emnapi/core': 1.10.0 @@ -14358,18 +15019,31 @@ snapshots: '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) optional: true + '@rolldown/binding-wasm32-wasi@1.2.0': + dependencies: + '@emnapi/core': 1.11.2 + '@emnapi/runtime': 1.11.2 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.2)(@emnapi/runtime@1.11.2) + optional: true + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.17': optional: true '@rolldown/binding-win32-arm64-msvc@1.1.5': optional: true + '@rolldown/binding-win32-arm64-msvc@1.2.0': + optional: true + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.17': optional: true '@rolldown/binding-win32-x64-msvc@1.1.5': optional: true + '@rolldown/binding-win32-x64-msvc@1.2.0': + optional: true + '@rolldown/plugin-babel@0.2.3(@babel/core@7.29.7)(@babel/plugin-transform-runtime@7.29.7(@babel/core@7.29.7))(@babel/runtime@7.29.7)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(rolldown@1.1.5)': dependencies: '@babel/core': 7.29.7 @@ -14468,6 +15142,38 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.61.0': optional: true + '@sapphire/async-queue@1.5.5': {} + + '@sapphire/shapeshift@4.0.0': + dependencies: + fast-deep-equal: 3.1.3 + lodash: 4.18.1 + + '@sapphire/snowflake@3.5.5': {} + + '@segment/analytics-core@1.8.2': + dependencies: + '@lukeed/uuid': 2.0.1 + '@segment/analytics-generic-utils': 1.2.0 + dset: 3.1.4 + tslib: 2.8.1 + + '@segment/analytics-generic-utils@1.2.0': + dependencies: + tslib: 2.8.1 + + '@segment/analytics-node@2.3.0': + dependencies: + '@lukeed/uuid': 2.0.1 + '@segment/analytics-core': 1.8.2 + '@segment/analytics-generic-utils': 1.2.0 + buffer: 6.0.3 + jose: 5.10.0 + node-fetch: 2.7.0 + tslib: 2.8.1 + transitivePeerDependencies: + - encoding + '@shikijs/core@4.2.0': dependencies: '@shikijs/primitive': 4.2.0 @@ -14764,6 +15470,11 @@ snapshots: '@tanstack/devtools-event-client': 0.4.3 '@tanstack/store': 0.8.1 + '@tanstack/pacer@0.20.1': + dependencies: + '@tanstack/devtools-event-client': 0.4.3 + '@tanstack/store': 0.9.3 + '@tanstack/query-core@5.100.14': {} '@tanstack/react-pacer@0.19.4(react-dom@19.2.6(react@19.2.6))(react@19.2.6)': @@ -15010,6 +15721,8 @@ snapshots: '@types/unist@3.0.3': {} + '@types/uuid@10.0.0': {} + '@types/webidl-conversions@7.0.3': {} '@types/whatwg-url@11.0.5': @@ -15090,12 +15803,25 @@ snapshots: '@testing-library/dom': 10.4.1 '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) '@vitest/browser': 4.1.9(@voidzero-dev/vite-plus-core@0.2.2(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(utf-8-validate@6.0.6)(vitest@4.1.9) - vitest: 4.1.9(@types/node@24.12.4)(@vitest/browser-preview@4.1.9)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3)) + vitest: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(@vitest/browser-preview@4.1.9)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3)) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/browser-preview@4.1.9(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(utf-8-validate@6.0.6)(vite@8.2.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(vitest@4.1.9)': + dependencies: + '@testing-library/dom': 10.4.1 + '@testing-library/user-event': 14.6.1(@testing-library/dom@10.4.1) + '@vitest/browser': 4.1.9(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(utf-8-validate@6.0.6)(vite@8.2.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(vitest@4.1.9) + vitest: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(@vitest/browser-preview@4.1.9)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(vite@8.2.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)) transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite + optional: true '@vitest/browser@4.1.9(@voidzero-dev/vite-plus-core@0.2.2(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(utf-8-validate@6.0.6)(vitest@4.1.9)': dependencies: @@ -15106,13 +15832,31 @@ snapshots: pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.1.0 - vitest: 4.1.9(@types/node@24.12.4)(@vitest/browser-preview@4.1.9)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3)) + vitest: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(@vitest/browser-preview@4.1.9)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3)) + ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) + transitivePeerDependencies: + - bufferutil + - msw + - utf-8-validate + - vite + + '@vitest/browser@4.1.9(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(utf-8-validate@6.0.6)(vite@8.2.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(vitest@4.1.9)': + dependencies: + '@blazediff/core': 1.9.1 + '@vitest/mocker': 4.1.9(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(vite@8.2.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)) + '@vitest/utils': 4.1.9 + magic-string: 0.30.21 + pngjs: 7.0.0 + sirv: 3.0.2 + tinyrainbow: 3.1.0 + vitest: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(@vitest/browser-preview@4.1.9)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(vite@8.2.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)) ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite + optional: true '@vitest/expect@4.1.9': dependencies: @@ -15132,6 +15876,16 @@ snapshots: msw: 2.12.11(@types/node@24.12.4)(typescript@6.0.3) vite: '@voidzero-dev/vite-plus-core@0.2.2(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0)' + '@vitest/mocker@4.1.9(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(vite@8.2.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))': + dependencies: + '@vitest/spy': 4.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + msw: 2.12.11(@types/node@24.12.4)(typescript@6.0.3) + vite: 8.2.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0) + optional: true + '@vitest/pretty-format@4.1.9': dependencies: tinyrainbow: 3.1.0 @@ -15156,6 +15910,8 @@ snapshots: convert-source-map: 2.0.0 tinyrainbow: 3.1.0 + '@vladfrangu/async_event_emitter@2.4.7': {} + '@voidzero-dev/vite-plus-core@0.2.2(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0)': dependencies: '@oxc-project/runtime': 0.138.0 @@ -15350,7 +16106,7 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 - alchemy@2.0.0-beta.65(a455401069e1fee89f31a277c51247f6): + alchemy@2.0.0-beta.65(cca023e78b21aab9077f3185bc54be18): dependencies: '@alchemy.run/node-utils': 0.0.5 '@aws-sdk/credential-providers': 3.1062.0 @@ -15395,7 +16151,7 @@ snapshots: '@effect/platform-node': 4.0.0-beta.103(bufferutil@4.1.0)(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9))(ioredis@5.11.0)(utf-8-validate@6.0.6) '@effect/sql-pg': 4.0.0-beta.103(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9)) drizzle-kit: 1.0.0-rc.4 - drizzle-orm: 1.0.0-rc.4(@cloudflare/workers-types@4.20260604.1)(@effect/sql-d1@4.0.0-beta.101(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9)))(@effect/sql-pg@4.0.0-beta.103(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9)))(@effect/sql-sqlite-bun@4.0.0-beta.103(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9)))(@libsql/client@0.17.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))(bun-types@1.3.14)(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9))(expo-sqlite@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(mysql2@3.22.4(@types/node@24.12.4))(pg@8.21.0)(zod@4.4.3) + drizzle-orm: 1.0.0-rc.4(@cloudflare/workers-types@4.20260604.1)(@effect/sql-d1@4.0.0-beta.101(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9)))(@effect/sql-pg@4.0.0-beta.103(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9)))(@effect/sql-sqlite-bun@4.0.0-beta.103(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9)))(@libsql/client@0.17.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@opentelemetry/api@1.9.1)(bun-types@1.3.14)(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9))(expo-sqlite@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(mysql2@3.22.4(@types/node@24.12.4))(pg@8.21.0)(zod@4.4.3) vite: '@voidzero-dev/vite-plus-core@0.2.2(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0)' ws: 8.21.0(bufferutil@4.1.0)(utf-8-validate@6.0.6) transitivePeerDependencies: @@ -15911,6 +16667,11 @@ snapshots: buffer-from@1.1.2: {} + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + bufferutil@4.1.0: dependencies: node-gyp-build: 4.8.4 @@ -16150,6 +16911,8 @@ snapshots: compare-version@0.1.2: {} + compare-versions@6.1.1: {} + compressible@2.0.18: dependencies: mime-db: 1.54.0 @@ -16365,6 +17128,27 @@ snapshots: minimatch: 3.1.5 p-limit: 3.1.0 + discord-api-types@0.38.52: {} + + discord.js@14.27.0(bufferutil@4.1.0)(utf-8-validate@6.0.6): + dependencies: + '@discordjs/builders': 1.14.1 + '@discordjs/collection': 1.5.3 + '@discordjs/formatters': 0.6.2 + '@discordjs/rest': 2.6.3 + '@discordjs/util': 1.2.0 + '@discordjs/ws': 1.2.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) + '@sapphire/snowflake': 3.5.5 + discord-api-types: 0.38.52 + fast-deep-equal: 3.1.3 + lodash.snakecase: 4.1.1 + magic-bytes.js: 1.13.1 + tslib: 2.8.1 + undici: 6.28.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + dmg-builder@26.15.6(electron-builder-squirrel-windows@26.15.6): dependencies: app-builder-lib: 26.15.6(dmg-builder@26.15.6)(electron-builder-squirrel-windows@26.15.6) @@ -16418,13 +17202,14 @@ snapshots: get-tsconfig: 4.14.0 jiti: 2.7.0 - drizzle-orm@1.0.0-rc.4(@cloudflare/workers-types@4.20260604.1)(@effect/sql-d1@4.0.0-beta.101(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9)))(@effect/sql-pg@4.0.0-beta.103(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9)))(@effect/sql-sqlite-bun@4.0.0-beta.103(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9)))(@libsql/client@0.17.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))(bun-types@1.3.14)(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9))(expo-sqlite@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(mysql2@3.22.4(@types/node@24.12.4))(pg@8.21.0)(zod@4.4.3): + drizzle-orm@1.0.0-rc.4(@cloudflare/workers-types@4.20260604.1)(@effect/sql-d1@4.0.0-beta.101(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9)))(@effect/sql-pg@4.0.0-beta.103(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9)))(@effect/sql-sqlite-bun@4.0.0-beta.103(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9)))(@libsql/client@0.17.3(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@opentelemetry/api@1.9.1)(bun-types@1.3.14)(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9))(expo-sqlite@56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6))(mysql2@3.22.4(@types/node@24.12.4))(pg@8.21.0)(zod@4.4.3): optionalDependencies: '@cloudflare/workers-types': 4.20260604.1 '@effect/sql-d1': 4.0.0-beta.101(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9)) '@effect/sql-pg': 4.0.0-beta.103(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9)) '@effect/sql-sqlite-bun': 4.0.0-beta.103(effect@4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9)) '@libsql/client': 0.17.3(bufferutil@4.1.0)(utf-8-validate@6.0.6) + '@opentelemetry/api': 1.9.1 bun-types: 1.3.14 effect: 4.0.0-beta.103(patch_hash=a18f963109656ddbeb2a99ca45f942ecf54830986259eb01ce6a970421c9c6a9) expo-sqlite: 56.0.5(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) @@ -16495,7 +17280,7 @@ snapshots: builder-util: 26.15.3 builder-util-runtime: 9.7.0 chalk: 4.1.2 - form-data: 4.0.5 + form-data: 4.0.6 fs-extra: 10.1.0 lazy-val: 1.0.5 mime: 2.6.0 @@ -17011,7 +17796,7 @@ snapshots: expo-symbols: 56.0.6(expo-font@56.0.7)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.3)(utf-8-validate@6.0.6))(react@19.2.3) fast-deep-equal: 3.1.3 invariant: 2.2.4 - nanoid: 3.3.12 + nanoid: 3.3.17 query-string: 7.1.3 react: 19.2.3 react-fast-compare: 3.2.2 @@ -17062,7 +17847,7 @@ snapshots: expo-symbols: 56.0.6(expo-font@56.0.7)(expo@56.0.12)(react-native@0.85.3(@babel/core@7.29.7)(@react-native/metro-config@0.85.3(@babel/core@7.29.7)(bufferutil@4.1.0)(utf-8-validate@6.0.6))(@types/react@19.2.16)(bufferutil@4.1.0)(react@19.2.6)(utf-8-validate@6.0.6))(react@19.2.6) fast-deep-equal: 3.1.3 invariant: 2.2.4 - nanoid: 3.3.12 + nanoid: 3.3.17 query-string: 7.1.3 react: 19.2.6 react-fast-compare: 3.2.2 @@ -17354,6 +18139,8 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-json-patch@3.1.1: {} + fast-json-stable-stringify@2.1.0: optional: true @@ -17482,7 +18269,7 @@ snapshots: dependencies: tiny-inflate: 1.0.3 - form-data@4.0.5: + form-data@4.0.6: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 @@ -17863,6 +18650,8 @@ snapshots: idb-keyval@6.2.1: optional: true + ieee754@1.2.1: {} + ignore@5.3.2: {} ignore@7.0.5: {} @@ -18064,6 +18853,8 @@ snapshots: jiti@2.7.0: {} + jose@5.10.0: {} + jose@6.2.2: {} jose@6.2.3: {} @@ -18188,6 +18979,9 @@ snapshots: lightningcss-android-arm64@1.32.0: optional: true + lightningcss-android-arm64@1.33.0: + optional: true + lightningcss-darwin-arm64@1.30.1: optional: true @@ -18197,6 +18991,9 @@ snapshots: lightningcss-darwin-arm64@1.32.0: optional: true + lightningcss-darwin-arm64@1.33.0: + optional: true + lightningcss-darwin-x64@1.30.1: optional: true @@ -18206,6 +19003,9 @@ snapshots: lightningcss-darwin-x64@1.32.0: optional: true + lightningcss-darwin-x64@1.33.0: + optional: true + lightningcss-freebsd-x64@1.30.1: optional: true @@ -18215,6 +19015,9 @@ snapshots: lightningcss-freebsd-x64@1.32.0: optional: true + lightningcss-freebsd-x64@1.33.0: + optional: true + lightningcss-linux-arm-gnueabihf@1.30.1: optional: true @@ -18224,6 +19027,9 @@ snapshots: lightningcss-linux-arm-gnueabihf@1.32.0: optional: true + lightningcss-linux-arm-gnueabihf@1.33.0: + optional: true + lightningcss-linux-arm64-gnu@1.30.1: optional: true @@ -18233,6 +19039,9 @@ snapshots: lightningcss-linux-arm64-gnu@1.32.0: optional: true + lightningcss-linux-arm64-gnu@1.33.0: + optional: true + lightningcss-linux-arm64-musl@1.30.1: optional: true @@ -18242,6 +19051,9 @@ snapshots: lightningcss-linux-arm64-musl@1.32.0: optional: true + lightningcss-linux-arm64-musl@1.33.0: + optional: true + lightningcss-linux-x64-gnu@1.30.1: optional: true @@ -18251,6 +19063,9 @@ snapshots: lightningcss-linux-x64-gnu@1.32.0: optional: true + lightningcss-linux-x64-gnu@1.33.0: + optional: true + lightningcss-linux-x64-musl@1.30.1: optional: true @@ -18260,6 +19075,9 @@ snapshots: lightningcss-linux-x64-musl@1.32.0: optional: true + lightningcss-linux-x64-musl@1.33.0: + optional: true + lightningcss-win32-arm64-msvc@1.30.1: optional: true @@ -18269,6 +19087,9 @@ snapshots: lightningcss-win32-arm64-msvc@1.32.0: optional: true + lightningcss-win32-arm64-msvc@1.33.0: + optional: true + lightningcss-win32-x64-msvc@1.30.1: optional: true @@ -18278,6 +19099,9 @@ snapshots: lightningcss-win32-x64-msvc@1.32.0: optional: true + lightningcss-win32-x64-msvc@1.33.0: + optional: true + lightningcss@1.30.1: dependencies: detect-libc: 2.1.2 @@ -18325,6 +19149,23 @@ snapshots: lightningcss-win32-arm64-msvc: 1.32.0 lightningcss-win32-x64-msvc: 1.32.0 + lightningcss@1.33.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.33.0 + lightningcss-darwin-arm64: 1.33.0 + lightningcss-darwin-x64: 1.33.0 + lightningcss-freebsd-x64: 1.33.0 + lightningcss-linux-arm-gnueabihf: 1.33.0 + lightningcss-linux-arm64-gnu: 1.33.0 + lightningcss-linux-arm64-musl: 1.33.0 + lightningcss-linux-x64-gnu: 1.33.0 + lightningcss-linux-x64-musl: 1.33.0 + lightningcss-win32-arm64-msvc: 1.33.0 + lightningcss-win32-x64-msvc: 1.33.0 + optional: true + locate-path@3.0.0: dependencies: p-locate: 3.0.0 @@ -18336,6 +19177,8 @@ snapshots: lodash.isequal@4.5.0: {} + lodash.snakecase@4.1.1: {} + lodash.throttle@4.1.1: {} lodash@4.18.1: {} @@ -18381,6 +19224,8 @@ snapshots: lz-string@1.5.0: {} + magic-bytes.js@1.13.1: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -19108,6 +19953,9 @@ snapshots: nanoid@3.3.12: {} + nanoid@3.3.17: + optional: true + negotiator@0.6.3: {} negotiator@0.6.4: {} @@ -19160,7 +20008,7 @@ snapshots: semver: 7.8.5 tar: 7.5.16 tinyglobby: 0.2.17 - undici: 6.26.0 + undici: 6.28.0 which: 6.0.1 node-int64@0.4.0: {} @@ -19313,7 +20161,7 @@ snapshots: outvariant@1.4.3: {} - oxfmt@0.57.0(vite-plus@0.2.2(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)): + oxfmt@0.57.0(vite-plus@0.2.2(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)): dependencies: tinypool: 2.1.0 optionalDependencies: @@ -19336,7 +20184,7 @@ snapshots: '@oxfmt/binding-win32-arm64-msvc': 0.57.0 '@oxfmt/binding-win32-ia32-msvc': 0.57.0 '@oxfmt/binding-win32-x64-msvc': 0.57.0 - vite-plus: 0.2.2(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + vite-plus: 0.2.2(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) oxlint-tsgolint@0.24.0: optionalDependencies: @@ -19347,7 +20195,7 @@ snapshots: '@oxlint-tsgolint/win32-arm64': 0.24.0 '@oxlint-tsgolint/win32-x64': 0.24.0 - oxlint@1.72.0(oxlint-tsgolint@0.24.0)(vite-plus@0.2.2(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)): + oxlint@1.72.0(oxlint-tsgolint@0.24.0)(vite-plus@0.2.2(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)): optionalDependencies: '@oxlint/binding-android-arm-eabi': 1.72.0 '@oxlint/binding-android-arm64': 1.72.0 @@ -19369,7 +20217,7 @@ snapshots: '@oxlint/binding-win32-ia32-msvc': 1.72.0 '@oxlint/binding-win32-x64-msvc': 1.72.0 oxlint-tsgolint: 0.24.0 - vite-plus: 0.2.2(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) + vite-plus: 0.2.2(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0) p-cancelable@2.1.1: {} @@ -19432,6 +20280,8 @@ snapshots: parseurl@1.3.3: {} + partial-json@0.1.7: {} + patch-console@2.0.0: {} path-browserify@1.0.1: {} @@ -19530,6 +20380,8 @@ snapshots: dependencies: split2: 4.2.0 + phoenix@1.8.9: {} + piccolore@0.1.3: {} picocolors@1.1.1: {} @@ -19538,6 +20390,8 @@ snapshots: picomatch@4.0.4: {} + picomatch@4.0.5: {} + pkce-challenge@5.0.1: {} pkg-up@3.1.0: @@ -19577,6 +20431,13 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + postcss@8.5.25: + dependencies: + nanoid: 3.3.17 + picocolors: 1.1.1 + source-map-js: 1.2.1 + optional: true + postgres-array@2.0.0: {} postgres-array@3.0.4: {} @@ -20418,6 +21279,28 @@ snapshots: '@rolldown/binding-win32-arm64-msvc': 1.1.5 '@rolldown/binding-win32-x64-msvc': 1.1.5 + rolldown@1.2.0: + dependencies: + '@oxc-project/types': 0.140.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.2.0 + '@rolldown/binding-darwin-arm64': 1.2.0 + '@rolldown/binding-darwin-x64': 1.2.0 + '@rolldown/binding-freebsd-x64': 1.2.0 + '@rolldown/binding-linux-arm-gnueabihf': 1.2.0 + '@rolldown/binding-linux-arm64-gnu': 1.2.0 + '@rolldown/binding-linux-arm64-musl': 1.2.0 + '@rolldown/binding-linux-ppc64-gnu': 1.2.0 + '@rolldown/binding-linux-s390x-gnu': 1.2.0 + '@rolldown/binding-linux-x64-gnu': 1.2.0 + '@rolldown/binding-linux-x64-musl': 1.2.0 + '@rolldown/binding-openharmony-arm64': 1.2.0 + '@rolldown/binding-wasm32-wasi': 1.2.0 + '@rolldown/binding-win32-arm64-msvc': 1.2.0 + '@rolldown/binding-win32-x64-msvc': 1.2.0 + optional: true + rollup@4.61.0: dependencies: '@types/estree': 1.0.9 @@ -20464,6 +21347,10 @@ snapshots: dependencies: queue-microtask: 1.2.3 + rxjs@7.8.1: + dependencies: + tslib: 2.8.1 + safe-buffer@5.1.2: {} safe-buffer@5.2.1: {} @@ -21021,6 +21908,8 @@ snapshots: ts-algebra@2.0.0: {} + ts-mixer@6.0.4: {} + tslib@2.8.1: {} type-fest@0.13.1: @@ -21058,7 +21947,7 @@ snapshots: undici-types@7.16.0: {} - undici@6.26.0: {} + undici@6.28.0: {} undici@7.27.1: {} @@ -21187,6 +22076,8 @@ snapshots: until-async@3.0.2: {} + untruncate-json@0.0.1: {} + unzipper@0.12.5: dependencies: bluebird: 3.7.2 @@ -21266,6 +22157,8 @@ snapshots: utils-merge@1.0.1: {} + uuid@11.1.1: {} + uuid@14.0.1: {} uuid@7.0.3: {} @@ -21308,7 +22201,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-plus@0.2.2(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0): + vite-plus@0.2.2(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0): dependencies: '@oxc-project/types': 0.138.0 '@oxlint/plugins': 1.68.0 @@ -21322,11 +22215,11 @@ snapshots: '@vitest/spy': 4.1.9 '@vitest/utils': 4.1.9 '@voidzero-dev/vite-plus-core': 0.2.2(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0) - oxfmt: 0.57.0(vite-plus@0.2.2(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)) - oxlint: 1.72.0(oxlint-tsgolint@0.24.0)(vite-plus@0.2.2(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)) + oxfmt: 0.57.0(vite-plus@0.2.2(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)) + oxlint: 1.72.0(oxlint-tsgolint@0.24.0)(vite-plus@0.2.2(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(bufferutil@4.1.0)(esbuild@0.28.1)(jiti@2.7.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(utf-8-validate@6.0.6)(yaml@2.9.0)) oxlint-tsgolint: 0.24.0 vite: '@voidzero-dev/vite-plus-core@0.2.2(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0)' - vitest: 4.1.9(@types/node@24.12.4)(@vitest/browser-preview@4.1.9)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3)) + vitest: 4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(@vitest/browser-preview@4.1.9)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3)) optionalDependencies: '@voidzero-dev/vite-plus-darwin-arm64': 0.2.2 '@voidzero-dev/vite-plus-darwin-x64': 0.2.2 @@ -21366,11 +22259,27 @@ snapshots: - utf-8-validate - yaml + vite@8.2.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0): + dependencies: + lightningcss: 1.33.0 + picomatch: 4.0.5 + postcss: 8.5.25 + rolldown: 1.2.0 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 24.12.4 + esbuild: 0.28.1 + fsevents: 2.3.3 + jiti: 2.7.0 + terser: 5.48.0 + yaml: 2.9.0 + optional: true + vitefu@1.1.3(@voidzero-dev/vite-plus-core@0.2.2(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0)): optionalDependencies: vite: '@voidzero-dev/vite-plus-core@0.2.2(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0)' - vitest@4.1.9(@types/node@24.12.4)(@vitest/browser-preview@4.1.9)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3)): + vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(@vitest/browser-preview@4.1.9)(@voidzero-dev/vite-plus-core@0.2.2(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3)): dependencies: '@vitest/expect': 4.1.9 '@vitest/mocker': 4.1.9(@voidzero-dev/vite-plus-core@0.2.2(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3)) @@ -21384,7 +22293,7 @@ snapshots: magic-string: 0.30.21 obug: 2.1.3 pathe: 2.0.3 - picomatch: 4.0.4 + picomatch: 4.0.5 std-env: 4.1.0 tinybench: 2.9.0 tinyexec: 1.2.4 @@ -21393,11 +22302,42 @@ snapshots: vite: '@voidzero-dev/vite-plus-core@0.2.2(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0)' why-is-node-running: 2.3.0 optionalDependencies: + '@opentelemetry/api': 1.9.1 '@types/node': 24.12.4 '@vitest/browser-preview': 4.1.9(@voidzero-dev/vite-plus-core@0.2.2(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(typescript@6.0.3)(unrun@0.2.39)(yaml@2.9.0))(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(utf-8-validate@6.0.6)(vitest@4.1.9) transitivePeerDependencies: - msw + vitest@4.1.9(@opentelemetry/api@1.9.1)(@types/node@24.12.4)(@vitest/browser-preview@4.1.9)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(vite@8.2.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)): + dependencies: + '@vitest/expect': 4.1.9 + '@vitest/mocker': 4.1.9(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(vite@8.2.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.9 + '@vitest/runner': 4.1.9 + '@vitest/snapshot': 4.1.9 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 + es-module-lexer: 2.1.0 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.3 + pathe: 2.0.3 + picomatch: 4.0.5 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.2.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@types/node': 24.12.4 + '@vitest/browser-preview': 4.1.9(bufferutil@4.1.0)(msw@2.12.11(@types/node@24.12.4)(typescript@6.0.3))(utf-8-validate@6.0.6)(vite@8.2.0(@types/node@24.12.4)(esbuild@0.28.1)(jiti@2.7.0)(terser@5.48.0)(yaml@2.9.0))(vitest@4.1.9) + transitivePeerDependencies: + - msw + optional: true + vlq@1.0.1: {} volar-service-css@0.0.70(@volar/language-service@2.4.28): @@ -21675,6 +22615,10 @@ snapshots: yoga-layout@3.2.1: {} + zod-to-json-schema@3.25.2(zod@3.25.76): + dependencies: + zod: 3.25.76 + zod-to-json-schema@3.25.2(zod@4.4.3): dependencies: zod: 4.4.3 From 64ad1a66677e45c028ca60252af88e8bee17fc1f Mon Sep 17 00:00:00 2001 From: Shivam Sharma <91240327+shivamhwp@users.noreply.github.com> Date: Fri, 7 Aug 2026 06:13:55 +0530 Subject: [PATCH 02/12] feat(channels): make Discord worktrees optional --- .../src/channels/T3CodeDiscordChannel.test.ts | 21 +++- .../src/channels/T3CodeDiscordChannel.ts | 113 +++++++++++++----- apps/server/src/serverSettings.test.ts | 2 + .../components/settings/ChannelSettings.tsx | 110 +++++++++++------ docs/README.md | 1 + docs/user/discord-channels.md | 10 ++ packages/contracts/src/settings.ts | 12 +- 7 files changed, 197 insertions(+), 72 deletions(-) create mode 100644 docs/user/discord-channels.md diff --git a/apps/server/src/channels/T3CodeDiscordChannel.test.ts b/apps/server/src/channels/T3CodeDiscordChannel.test.ts index 0fa33e6440a..2177adb0e8f 100644 --- a/apps/server/src/channels/T3CodeDiscordChannel.test.ts +++ b/apps/server/src/channels/T3CodeDiscordChannel.test.ts @@ -1,12 +1,16 @@ import { expect, it } from "@effect/vitest"; -import { ProjectId } from "@t3tools/contracts"; +import { DiscordChannelSettings, ProjectId } from "@t3tools/contracts"; +import * as Schema from "effect/Schema"; import { describe } from "vite-plus/test"; import { channelBranchName, isDiscordChannelConfigured } from "./T3CodeDiscordChannel.ts"; +const decodeDiscordChannelSettings = Schema.decodeSync(DiscordChannelSettings); + const configuredDiscord = { enabled: true, projectId: ProjectId.make("project-1"), + threadEnvMode: "worktree", baseBranch: "main", branchPrefix: "demo/discord", applicationId: "app-1", @@ -16,6 +20,10 @@ const configuredDiscord = { } as const; describe("Discord channel isolation", () => { + it("keeps isolated worktrees as the default for existing settings", () => { + expect(decodeDiscordChannelSettings({}).threadEnvMode).toBe("worktree"); + }); + it("creates a unique task branch below the configured prefix", () => { expect( channelBranchName({ @@ -36,6 +44,17 @@ describe("Discord channel isolation", () => { expect(isDiscordChannelConfigured({ ...configuredDiscord, branchPrefix: "" })).toBe(false); }); + it("does not require branch settings when tasks run in the project checkout", () => { + expect( + isDiscordChannelConfigured({ + ...configuredDiscord, + threadEnvMode: "local", + baseBranch: "", + branchPrefix: "", + }), + ).toBe(true); + }); + it("refuses to start while the integration is disabled", () => { expect(isDiscordChannelConfigured({ ...configuredDiscord, enabled: false })).toBe(false); }); diff --git a/apps/server/src/channels/T3CodeDiscordChannel.ts b/apps/server/src/channels/T3CodeDiscordChannel.ts index c153c543e02..63648b60285 100644 --- a/apps/server/src/channels/T3CodeDiscordChannel.ts +++ b/apps/server/src/channels/T3CodeDiscordChannel.ts @@ -24,6 +24,7 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Ref from "effect/Ref"; +import * as Schema from "effect/Schema"; import * as Stream from "effect/Stream"; import { GitWorkflowService } from "../git/GitWorkflowService.ts"; @@ -41,7 +42,8 @@ const MAX_BRANCH_SLUG_LENGTH = 40; export interface ChannelTaskStatus { readonly threadId: ThreadId; readonly title: string; - readonly branch: string; + readonly branch: string | null; + readonly threadEnvMode: "local" | "worktree"; readonly state: "queued" | "running" | "done" | "failed"; } @@ -79,13 +81,18 @@ export function isDiscordChannelConfigured(config: DiscordChannelSettings): bool return ( config.enabled && config.projectId !== null && - config.baseBranch.trim().length > 0 && - config.branchPrefix.trim().length > 0 && + (config.threadEnvMode === "local" || + (config.baseBranch.trim().length > 0 && config.branchPrefix.trim().length > 0)) && config.applicationId.length > 0 && config.botToken.length > 0 ); } +class DiscordChannelTaskError extends Schema.TaggedErrorClass()( + "DiscordChannelTaskError", + { message: Schema.String }, +) {} + export function channelBranchName(input: { readonly prefix: string; readonly prompt: string; @@ -134,12 +141,20 @@ function statusCard(status: ChannelTaskStatus) { Fields({ children: [ Field({ label: "Status", children: taskStateLabel(status.state) }), - Field({ label: "Branch", children: `\`${status.branch}\`` }), + Field({ + label: status.threadEnvMode === "worktree" ? "Branch" : "Target", + children: + status.threadEnvMode === "worktree" + ? `\`${status.branch ?? "worktree"}\`` + : "Project checkout", + }), ], }), Context({ children: - "This task is running in an isolated worktree. The base branch is never checked out for agent work.", + status.threadEnvMode === "worktree" + ? "This task is running in an isolated worktree." + : "This task is running directly in the project's current checkout.", }), ], }); @@ -158,7 +173,13 @@ function startedCard( Fields({ children: [ Field({ label: "Status", children: "Queued" }), - Field({ label: "Branch", children: `\`${task.branch}\`` }), + Field({ + label: task.threadEnvMode === "worktree" ? "Branch" : "Target", + children: + task.threadEnvMode === "worktree" + ? `\`${task.branch ?? "worktree"}\`` + : "Project checkout", + }), ], }), Actions({ @@ -189,7 +210,13 @@ function completedCard(input: { children: [ Field({ label: "Status", children: "Done" }), Field({ label: "Diff", children: fileLabel }), - Field({ label: "Branch", children: `\`${input.task.branch}\`` }), + Field({ + label: input.task.threadEnvMode === "worktree" ? "Branch" : "Target", + children: + input.task.threadEnvMode === "worktree" + ? `\`${input.task.branch ?? "worktree"}\`` + : "Project checkout", + }), ], }), Context({ children: "Open T3 Code to inspect the full transcript and diff." }), @@ -271,7 +298,9 @@ function createT3CodeChannel(input: { Header({ children: "Task did not start" }), Section({ children: - "T3 Code could not create an isolated worktree. The task was stopped before the agent ran.", + input.config.threadEnvMode === "worktree" + ? "T3 Code could not create an isolated worktree. The task was stopped before the agent ran." + : "T3 Code could not start the task in the project checkout. Check the project and provider configuration.", }), ], }), @@ -311,24 +340,11 @@ const makeOperations = Effect.gen(function* () { return `${prefix}-${uuid}`; }); - const startTaskEffect = Effect.fn("T3CodeDiscordChannel.startTask")(function* ( + const createTaskWorktree = Effect.fn("T3CodeDiscordChannel.createTaskWorktree")(function* ( prompt: string, config: DiscordChannelSettings, + workspaceRoot: string, ) { - if (config.projectId === null) { - return yield* Effect.fail("Discord channel project is not configured"); - } - const projectOption = yield* projectionSnapshotQuery.getProjectShellById(config.projectId); - if (Option.isNone(projectOption)) { - return yield* Effect.fail("Discord channel project was not found"); - } - const project = projectOption.value; - if (project.defaultModelSelection === null) { - return yield* Effect.fail("Discord channel project has no default model"); - } - - const now = DateTime.formatIso(yield* DateTime.now); - const threadId = ThreadId.make(yield* nextId("channel-thread")); const suffix = (yield* nextId("branch")).slice(-8); const branch = channelBranchName({ prefix: config.branchPrefix, @@ -336,16 +352,47 @@ const makeOperations = Effect.gen(function* () { suffix, }); if (branch === config.baseBranch) { - return yield* Effect.fail("Discord channel branch must differ from its base branch"); + return yield* new DiscordChannelTaskError({ + message: "Discord channel branch must differ from its base branch", + }); } - - const worktree = yield* gitWorkflow.createWorktree({ - cwd: project.workspaceRoot, + return yield* gitWorkflow.createWorktree({ + cwd: workspaceRoot, refName: config.baseBranch, baseRefName: config.baseBranch, newRefName: branch, path: null, }); + }); + + const startTaskEffect = Effect.fn("T3CodeDiscordChannel.startTask")(function* ( + prompt: string, + config: DiscordChannelSettings, + ) { + if (config.projectId === null) { + return yield* new DiscordChannelTaskError({ + message: "Discord channel project is not configured", + }); + } + const projectOption = yield* projectionSnapshotQuery.getProjectShellById(config.projectId); + if (Option.isNone(projectOption)) { + return yield* new DiscordChannelTaskError({ + message: "Discord channel project was not found", + }); + } + const project = projectOption.value; + if (project.defaultModelSelection === null) { + return yield* new DiscordChannelTaskError({ + message: "Discord channel project has no default model", + }); + } + + const now = DateTime.formatIso(yield* DateTime.now); + const threadId = ThreadId.make(yield* nextId("channel-thread")); + const worktree = + config.threadEnvMode === "worktree" + ? yield* createTaskWorktree(prompt, config, project.workspaceRoot) + : null; const title = promptTitle(prompt); yield* orchestrationEngine.dispatch({ type: "thread.create", @@ -356,8 +403,8 @@ const makeOperations = Effect.gen(function* () { modelSelection: project.defaultModelSelection, runtimeMode: "full-access", interactionMode: "default", - branch: worktree.worktree.refName, - worktreePath: worktree.worktree.path, + branch: worktree?.worktree.refName ?? null, + worktreePath: worktree?.worktree.path ?? null, createdAt: now, }); yield* orchestrationEngine.dispatch({ @@ -377,7 +424,8 @@ const makeOperations = Effect.gen(function* () { return { threadId, title, - branch: worktree.worktree.refName, + branch: worktree?.worktree.refName ?? null, + threadEnvMode: config.threadEnvMode, state: "queued" as const, }; }); @@ -398,10 +446,12 @@ const makeOperations = Effect.gen(function* () { } return "queued" as const; })(); + const threadEnvMode = thread.worktreePath === null ? ("local" as const) : ("worktree" as const); return { threadId, title: thread.title, - branch: thread.branch ?? "isolated worktree", + branch: thread.branch, + threadEnvMode, state, }; }); @@ -416,6 +466,7 @@ function configFingerprint(config: DiscordChannelSettings): string { return [ config.enabled, config.projectId, + config.threadEnvMode, config.baseBranch, config.branchPrefix, config.applicationId, diff --git a/apps/server/src/serverSettings.test.ts b/apps/server/src/serverSettings.test.ts index fe45faa2343..93e98948538 100644 --- a/apps/server/src/serverSettings.test.ts +++ b/apps/server/src/serverSettings.test.ts @@ -704,6 +704,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { discord: { enabled: true, projectId: ProjectId.make("project-1"), + threadEnvMode: "local", baseBranch: "main", branchPrefix: "demo/discord", applicationId: "app-1", @@ -715,6 +716,7 @@ it.layer(NodeServices.layer)("server settings", (it) => { }); assert.equal(next.channelIntegrations.discord.botToken, "discord-secret"); + assert.equal(next.channelIntegrations.discord.threadEnvMode, "local"); const raw = yield* fileSystem.readFileString(serverConfig.settingsPath); assert.notInclude(raw, "discord-secret"); diff --git a/apps/web/src/components/settings/ChannelSettings.tsx b/apps/web/src/components/settings/ChannelSettings.tsx index c1bd16c6518..ab1321b8c13 100644 --- a/apps/web/src/components/settings/ChannelSettings.tsx +++ b/apps/web/src/components/settings/ChannelSettings.tsx @@ -52,6 +52,7 @@ export function ChannelSettings() { ); const [enabled, setEnabled] = useState(settings.enabled); const [projectId, setProjectId] = useState(settings.projectId); + const [threadEnvMode, setThreadEnvMode] = useState(settings.threadEnvMode); const [baseBranch, setBaseBranch] = useState(settings.baseBranch); const [branchPrefix, setBranchPrefix] = useState(settings.branchPrefix); const [applicationId, setApplicationId] = useState(settings.applicationId); @@ -62,6 +63,7 @@ export function ChannelSettings() { useEffect(() => { setEnabled(settings.enabled); setProjectId(settings.projectId); + setThreadEnvMode(settings.threadEnvMode); setBaseBranch(settings.baseBranch); setBranchPrefix(settings.branchPrefix); setApplicationId(settings.applicationId); @@ -71,8 +73,8 @@ export function ChannelSettings() { const hasBotToken = botTokenChanged ? botToken.length > 0 : settings.botTokenRedacted; const setupComplete = projectId !== null && - baseBranch.trim().length > 0 && - branchPrefix.trim().length > 0 && + (threadEnvMode === "local" || + (baseBranch.trim().length > 0 && branchPrefix.trim().length > 0)) && applicationId.trim().length > 0 && hasBotToken; const selectedProject = projects.find((project) => project.id === projectId) ?? null; @@ -83,6 +85,7 @@ export function ChannelSettings() { discord: { enabled, projectId, + threadEnvMode, baseBranch, branchPrefix, applicationId, @@ -173,45 +176,82 @@ export function ChannelSettings() { - }> + }> setThreadEnvMode(value === "local" ? "local" : "worktree")} + > + + + {threadEnvMode === "worktree" ? "Isolated worktree" : "Project checkout"} + + + + + Isolated worktree + + + Project checkout + + + + } + /> + - Base branch protected + + + {threadEnvMode === "worktree" ? "Isolated" : "Not isolated"} } /> - -
-
- - setBaseBranch(event.currentTarget.value)} - placeholder="main" - aria-label="Channel base branch" - /> -
-
- - setBranchPrefix(event.currentTarget.value)} - placeholder="demo/discord" - aria-label="Channel branch prefix" - /> + {threadEnvMode === "worktree" ? ( + +
+
+ + setBaseBranch(event.currentTarget.value)} + placeholder="main" + aria-label="Channel base branch" + /> +
+
+ + setBranchPrefix(event.currentTarget.value)} + placeholder="demo/discord" + aria-label="Channel branch prefix" + /> +
-
- + + ) : null}
@@ -183,7 +255,11 @@ export function ChannelSettings() { control={ setBaseBranch(event.currentTarget.value)} + onChange={(event) => { + setBaseBranch(event.currentTarget.value); + dirtyFieldsRef.current.add("baseBranch"); + setSaveStatus("unsaved"); + }} + onBlur={() => void persistDiscordPatch({ baseBranch }, ["baseBranch"])} placeholder="main" aria-label="Channel base branch" /> @@ -244,7 +325,12 @@ export function ChannelSettings() { setBranchPrefix(event.currentTarget.value)} + onChange={(event) => { + setBranchPrefix(event.currentTarget.value); + dirtyFieldsRef.current.add("branchPrefix"); + setSaveStatus("unsaved"); + }} + onBlur={() => void persistDiscordPatch({ branchPrefix }, ["branchPrefix"])} placeholder="demo/discord" aria-label="Channel branch prefix" /> @@ -253,8 +339,17 @@ export function ChannelSettings() { ) : null}
-
diff --git a/apps/web/src/hooks/useSettings.ts b/apps/web/src/hooks/useSettings.ts index e58876b19f7..b39e6781a23 100644 --- a/apps/web/src/hooks/useSettings.ts +++ b/apps/web/src/hooks/useSettings.ts @@ -29,6 +29,7 @@ import { APP_STAGE_LABEL } from "~/branding"; import { resolveSidebarV2Enabled } from "~/branding.logic"; import { ensureLocalApi } from "~/localApi"; import * as Struct from "effect/Struct"; +import { AsyncResult } from "effect/unstable/reactivity"; import { primaryServerSettingsAtom, serverEnvironment } from "~/state/server"; import { usePrimaryEnvironment } from "~/state/environments"; import { useAtomCommand } from "~/state/use-atom-command"; @@ -323,6 +324,27 @@ export function useUpdatePrimarySettings() { return useUpdateSettingsTarget(usePrimaryEnvironment()?.environmentId ?? null); } +/** Persist a primary-environment server patch and report when the local write has completed. */ +export function usePersistPrimaryServerSettings() { + const environmentId = usePrimaryEnvironment()?.environmentId ?? null; + const persistServerSettings = useAtomCommand( + serverEnvironment.updateSettings, + "server settings update", + ); + + return useCallback( + async (patch: ServerSettingsPatch): Promise => { + if (environmentId === null) return false; + const result = await persistServerSettings({ + environmentId, + input: { patch }, + }); + return AsyncResult.isSuccess(result); + }, + [environmentId, persistServerSettings], + ); +} + export function useUpdateClientSettings() { return useCallback((patch: ClientSettingsPatch) => { persistClientSettings({ From 61fb3e02b2f72538ceff94d2eb81ded248e6d5f5 Mon Sep 17 00:00:00 2001 From: Shivam Sharma <91240327+shivamhwp@users.noreply.github.com> Date: Fri, 7 Aug 2026 07:16:07 +0530 Subject: [PATCH 04/12] feat(channels): add Discord server install link --- .../components/settings/ChannelSettings.tsx | 21 ++++++++++++++-- .../settings/discordInstallUrl.test.ts | 25 +++++++++++++++++++ .../components/settings/discordInstallUrl.ts | 17 +++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/components/settings/discordInstallUrl.test.ts create mode 100644 apps/web/src/components/settings/discordInstallUrl.ts diff --git a/apps/web/src/components/settings/ChannelSettings.tsx b/apps/web/src/components/settings/ChannelSettings.tsx index d597892c718..674a4b7a919 100644 --- a/apps/web/src/components/settings/ChannelSettings.tsx +++ b/apps/web/src/components/settings/ChannelSettings.tsx @@ -1,8 +1,9 @@ -import { BotIcon, GitBranchIcon, ShieldCheckIcon } from "lucide-react"; +import { BotIcon, ExternalLinkIcon, GitBranchIcon, ShieldCheckIcon } from "lucide-react"; import { ProjectId, type ServerSettingsPatch } from "@t3tools/contracts"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { usePersistPrimaryServerSettings, usePrimarySettings } from "../../hooks/useSettings"; +import { ensureLocalApi } from "../../localApi"; import { useProjects } from "../../state/entities"; import { usePrimaryEnvironment } from "../../state/environments"; import { Badge } from "../ui/badge"; @@ -12,6 +13,7 @@ import { Select, SelectItem, SelectPopup, SelectTrigger, SelectValue } from "../ import { Switch } from "../ui/switch"; import { SettingsPageContainer, SettingsRow, SettingsSection } from "./settingsLayout"; import { searchableSetting } from "./settingsSearch"; +import { buildDiscordInstallUrl } from "./discordInstallUrl"; function SecretInput({ label, @@ -94,6 +96,7 @@ export function ChannelSettings() { applicationId.trim().length > 0 && hasBotToken; const selectedProject = projects.find((project) => project.id === projectId) ?? null; + const discordInstallUrl = buildDiscordInstallUrl(applicationId, guildId); const persistDiscordPatch = useCallback( async (discord: DiscordChannelPatch, persistedFields: ReadonlyArray = []) => { @@ -198,7 +201,7 @@ export function ChannelSettings() { />
+
diff --git a/apps/web/src/components/settings/discordInstallUrl.test.ts b/apps/web/src/components/settings/discordInstallUrl.test.ts new file mode 100644 index 00000000000..a69140cfd8e --- /dev/null +++ b/apps/web/src/components/settings/discordInstallUrl.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from "vite-plus/test"; + +import { buildDiscordInstallUrl } from "./discordInstallUrl"; + +describe("buildDiscordInstallUrl", () => { + it("requests only the scopes and permissions needed by the Discord channel", () => { + const result = buildDiscordInstallUrl("1535085613399933028", ""); + expect(result).not.toBeNull(); + + const url = new URL(result!); + expect(url.origin + url.pathname).toBe("https://discord.com/oauth2/authorize"); + expect(Object.fromEntries(url.searchParams)).toEqual({ + client_id: "1535085613399933028", + integration_type: "0", + permissions: "309237713920", + scope: "bot applications.commands", + }); + }); + + it("prefills a valid server ID and rejects an invalid application ID", () => { + const result = buildDiscordInstallUrl(" 1535085613399933028 ", " 123456789012345678 "); + expect(new URL(result!).searchParams.get("guild_id")).toBe("123456789012345678"); + expect(buildDiscordInstallUrl("not-an-id", "123456789012345678")).toBeNull(); + }); +}); diff --git a/apps/web/src/components/settings/discordInstallUrl.ts b/apps/web/src/components/settings/discordInstallUrl.ts new file mode 100644 index 00000000000..225fcd94a38 --- /dev/null +++ b/apps/web/src/components/settings/discordInstallUrl.ts @@ -0,0 +1,17 @@ +const DISCORD_CHANNEL_PERMISSIONS = "309237713920"; + +export function buildDiscordInstallUrl(applicationId: string, guildId: string): string | null { + const clientId = applicationId.trim(); + if (!/^\d+$/.test(clientId)) return null; + + const searchParams = new URLSearchParams({ + client_id: clientId, + integration_type: "0", + permissions: DISCORD_CHANNEL_PERMISSIONS, + scope: "bot applications.commands", + }); + const serverId = guildId.trim(); + if (/^\d+$/.test(serverId)) searchParams.set("guild_id", serverId); + + return `https://discord.com/oauth2/authorize?${searchParams.toString()}`; +} From d817e55d872aa1a587f308fd2be4ee1dbe987419 Mon Sep 17 00:00:00 2001 From: Shivam Sharma <91240327+shivamhwp@users.noreply.github.com> Date: Fri, 7 Aug 2026 07:51:01 +0530 Subject: [PATCH 05/12] feat(channels): select Discord task models --- apps/server/package.json | 3 +- .../src/channels/T3CodeDiscordChannel.test.ts | 72 +++++++- .../src/channels/T3CodeDiscordChannel.ts | 170 ++++++++++++++++-- pnpm-lock.yaml | 3 + 4 files changed, 228 insertions(+), 20 deletions(-) diff --git a/apps/server/package.json b/apps/server/package.json index ed9d6623907..657d436b0ae 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -35,7 +35,8 @@ "@pierre/diffs": "catalog:", "effect": "catalog:", "node-pty": "^1.1.0", - "yaml": "catalog:" + "yaml": "catalog:", + "zod": "^4.4.3" }, "devDependencies": { "@effect/vitest": "catalog:", diff --git a/apps/server/src/channels/T3CodeDiscordChannel.test.ts b/apps/server/src/channels/T3CodeDiscordChannel.test.ts index 2177adb0e8f..96ee6de4ae6 100644 --- a/apps/server/src/channels/T3CodeDiscordChannel.test.ts +++ b/apps/server/src/channels/T3CodeDiscordChannel.test.ts @@ -1,9 +1,20 @@ import { expect, it } from "@effect/vitest"; -import { DiscordChannelSettings, ProjectId } from "@t3tools/contracts"; +import { + DiscordChannelSettings, + ProjectId, + ProviderDriverKind, + ProviderInstanceId, + type ServerProvider, +} from "@t3tools/contracts"; import * as Schema from "effect/Schema"; import { describe } from "vite-plus/test"; -import { channelBranchName, isDiscordChannelConfigured } from "./T3CodeDiscordChannel.ts"; +import { + channelBranchName, + discordModelOptions, + isDiscordChannelConfigured, + resolveDiscordModel, +} from "./T3CodeDiscordChannel.ts"; const decodeDiscordChannelSettings = Schema.decodeSync(DiscordChannelSettings); @@ -59,3 +70,60 @@ describe("Discord channel isolation", () => { expect(isDiscordChannelConfigured({ ...configuredDiscord, enabled: false })).toBe(false); }); }); + +describe("Discord channel model selection", () => { + const provider = { + instanceId: ProviderInstanceId.make("codex"), + driver: ProviderDriverKind.make("codex"), + displayName: "Codex", + enabled: true, + installed: true, + version: null, + status: "ready", + availability: "available", + auth: { status: "authenticated" }, + checkedAt: "2026-08-07T00:00:00.000Z", + models: [ + { + slug: "gpt-5.6-sol", + name: "GPT-5.6 Sol", + isCustom: false, + capabilities: null, + }, + { + slug: "gpt-5.4", + name: "GPT-5.4", + isCustom: false, + isLegacy: true, + capabilities: null, + }, + ], + slashCommands: [], + skills: [], + } satisfies ServerProvider; + + it("offers current models from runnable provider instances", () => { + expect(discordModelOptions([provider])).toEqual([ + { + value: "codex/gpt-5.6-sol", + label: "Codex · GPT-5.6 Sol", + selection: { + instanceId: ProviderInstanceId.make("codex"), + model: "gpt-5.6-sol", + }, + }, + ]); + expect( + discordModelOptions([{ ...provider, auth: { status: "unauthenticated" } } as ServerProvider]), + ).toEqual([]); + }); + + it("routes the selected Discord value to the exact provider and model", () => { + const models = discordModelOptions([provider]); + expect(resolveDiscordModel(models, "codex/gpt-5.6-sol")?.selection).toEqual({ + instanceId: ProviderInstanceId.make("codex"), + model: "gpt-5.6-sol", + }); + expect(resolveDiscordModel(models, "codex/missing")).toBeUndefined(); + }); +}); diff --git a/apps/server/src/channels/T3CodeDiscordChannel.ts b/apps/server/src/channels/T3CodeDiscordChannel.ts index 63648b60285..68aabfee03d 100644 --- a/apps/server/src/channels/T3CodeDiscordChannel.ts +++ b/apps/server/src/channels/T3CodeDiscordChannel.ts @@ -1,4 +1,4 @@ -import { createChannel } from "@copilotkit/channels-core"; +import { createChannel, defineChannelCommand } from "@copilotkit/channels-core"; import { discord } from "@copilotkit/channels-discord"; import { Actions, @@ -15,6 +15,8 @@ import { CommandId, type DiscordChannelSettings, MessageId, + type ModelSelection, + type ServerProvider, type ServerSettings, ThreadId, } from "@t3tools/contracts"; @@ -26,10 +28,12 @@ import * as Option from "effect/Option"; import * as Ref from "effect/Ref"; import * as Schema from "effect/Schema"; import * as Stream from "effect/Stream"; +import { z } from "zod"; import { GitWorkflowService } from "../git/GitWorkflowService.ts"; import { OrchestrationEngineService } from "../orchestration/Services/OrchestrationEngine.ts"; import { ProjectionSnapshotQuery } from "../orchestration/Services/ProjectionSnapshotQuery.ts"; +import { ProviderRegistry } from "../provider/Services/ProviderRegistry.ts"; import { forkParked } from "../serverActivation.ts"; import { ServerSettingsService } from "../serverSettings.ts"; @@ -44,6 +48,7 @@ export interface ChannelTaskStatus { readonly title: string; readonly branch: string | null; readonly threadEnvMode: "local" | "worktree"; + readonly modelSelection: ModelSelection; readonly state: "queued" | "running" | "done" | "failed"; } @@ -55,12 +60,50 @@ export interface T3CodeChannelOperations { readonly startTask: ( prompt: string, config: DiscordChannelSettings, + modelSelection?: ModelSelection, ) => Promise; readonly getTaskStatus: (threadId: ThreadId) => Promise; + readonly listModels: () => Promise>; } interface LinkedConversationState { - readonly t3ThreadId: string; + readonly t3ThreadId?: string; + readonly modelSelection?: ModelSelection; +} + +export interface DiscordModelOption { + readonly value: string; + readonly label: string; + readonly selection: ModelSelection; +} + +export function discordModelOptions( + providers: ReadonlyArray, +): ReadonlyArray { + return providers.flatMap((provider) => { + if ( + !provider.enabled || + !provider.installed || + provider.auth.status === "unauthenticated" || + provider.availability === "unavailable" + ) { + return []; + } + return provider.models + .filter((model) => model.isLegacy !== true) + .map((model) => ({ + value: `${provider.instanceId}/${model.slug}`, + label: `${provider.displayName ?? provider.instanceId} · ${model.name}`, + selection: { instanceId: provider.instanceId, model: model.slug }, + })); + }); +} + +export function resolveDiscordModel( + models: ReadonlyArray, + value: string, +): DiscordModelOption | undefined { + return models.find((model) => model.value === value); } type ChannelThread = Pick & { @@ -132,6 +175,25 @@ function taskStateLabel(state: ChannelTaskStatus["state"]): string { } } +function modelLabel(selection: ModelSelection): string { + return `${selection.instanceId}/${selection.model}`; +} + +function readConversationState(value: unknown): LinkedConversationState { + if (typeof value !== "object" || value === null) return {}; + const record = value as Record; + const t3ThreadId = typeof record.t3ThreadId === "string" ? record.t3ThreadId : undefined; + const candidate = record.modelSelection; + const modelSelection = + typeof candidate === "object" && + candidate !== null && + typeof (candidate as Record).instanceId === "string" && + typeof (candidate as Record).model === "string" + ? (candidate as ModelSelection) + : undefined; + return { ...(t3ThreadId ? { t3ThreadId } : {}), ...(modelSelection ? { modelSelection } : {}) }; +} + function statusCard(status: ChannelTaskStatus) { return Message({ accent: status.state === "failed" ? FAILED_ACCENT : DISCORD_ACCENT, @@ -141,6 +203,7 @@ function statusCard(status: ChannelTaskStatus) { Fields({ children: [ Field({ label: "Status", children: taskStateLabel(status.state) }), + Field({ label: "Model", children: `\`${modelLabel(status.modelSelection)}\`` }), Field({ label: status.threadEnvMode === "worktree" ? "Branch" : "Target", children: @@ -173,6 +236,7 @@ function startedCard( Fields({ children: [ Field({ label: "Status", children: "Queued" }), + Field({ label: "Model", children: `\`${modelLabel(task.modelSelection)}\`` }), Field({ label: task.threadEnvMode === "worktree" ? "Branch" : "Target", children: @@ -210,6 +274,7 @@ function completedCard(input: { children: [ Field({ label: "Status", children: "Done" }), Field({ label: "Diff", children: fileLabel }), + Field({ label: "Model", children: `\`${modelLabel(input.task.modelSelection)}\`` }), Field({ label: input.task.threadEnvMode === "worktree" ? "Branch" : "Target", children: @@ -227,6 +292,7 @@ function completedCard(input: { function createT3CodeChannel(input: { readonly config: DiscordChannelSettings; readonly operations: T3CodeChannelOperations; + readonly models: ReadonlyArray; }) { const linkedThreads = new Map>(); const channel = createChannel({ @@ -255,14 +321,7 @@ function createT3CodeChannel(input: { const handleText = async (thread: ChannelThread, rawText: string) => { const text = cleanDiscordPrompt(rawText); - const storedState = await thread.state(); - const state = - typeof storedState === "object" && - storedState !== null && - "t3ThreadId" in storedState && - typeof storedState.t3ThreadId === "string" - ? ({ t3ThreadId: storedState.t3ThreadId } satisfies LinkedConversationState) - : undefined; + const state = readConversationState(await thread.state()); if (text.toLocaleLowerCase() === "status") { if (!state?.t3ThreadId) { await thread.post("No T3 Code task is linked to this Discord thread yet."); @@ -285,8 +344,11 @@ function createT3CodeChannel(input: { } try { - const task = await input.operations.startTask(text, input.config); - await thread.setState({ t3ThreadId: task.threadId } satisfies LinkedConversationState); + const task = await input.operations.startTask(text, input.config, state.modelSelection); + await thread.setState({ + ...state, + t3ThreadId: task.threadId, + } satisfies LinkedConversationState); linkedThreads.set(task.threadId, thread); await thread.post(startedCard(task, (target) => postStatus(target, task.threadId))); } catch { @@ -309,7 +371,70 @@ function createT3CodeChannel(input: { }; channel.onMention(({ thread, message }) => handleText(thread, message.text)); - channel.onCommand("t3", ({ thread, text }) => handleText(thread, text)); + channel.onCommand( + defineChannelCommand({ + name: "t3", + description: "Run a coding task in T3 Code with the selected model.", + options: z.object({ + prompt: z.string().min(1).describe("The coding task for T3 Code"), + }), + handler: ({ thread, text, options }) => handleText(thread, options.prompt ?? text), + }), + ); + channel.onCommand( + defineChannelCommand({ + name: "status", + description: "Show the status of the T3 Code task linked to this channel.", + handler: ({ thread }) => handleText(thread, "status"), + }), + ); + + const selectableModels = input.models.slice(0, 25); + const modelValues = selectableModels.map(({ value }) => value); + const modelSchema = + modelValues.length > 0 ? z.enum(modelValues as [string, ...string[]]) : z.string().min(1); + channel.onCommand( + defineChannelCommand({ + name: "model", + description: "Choose the model used by future T3 Code tasks in this channel.", + options: z.object({ + model: modelSchema.describe("Provider and model"), + }), + async handler({ thread, options }) { + const selected = resolveDiscordModel(selectableModels, options.model); + if (!selected) { + await thread.post( + "That model is not currently available. Run `/models` to see the list.", + ); + return; + } + const state = readConversationState(await thread.state()); + await thread.setState({ + ...state, + modelSelection: selected.selection, + } satisfies LinkedConversationState); + await thread.post( + `Future T3 Code tasks in this channel will use **${selected.label}** (\`${selected.value}\`).`, + ); + }, + }), + ); + channel.onCommand( + defineChannelCommand({ + name: "models", + description: "List models available to T3 Code and show the current selection.", + async handler({ thread }) { + const state = readConversationState(await thread.state()); + const current = state.modelSelection ? modelLabel(state.modelSelection) : "project default"; + const list = selectableModels + .map(({ value, label }) => `• \`${value}\` — ${label}`) + .join("\n"); + await thread.post( + `**Current model:** ${current === "project default" ? current : `\`${current}\``}\n\n${list || "No runnable models are currently available."}`, + ); + }, + }), + ); return { channel, @@ -332,6 +457,7 @@ const makeOperations = Effect.gen(function* () { const gitWorkflow = yield* GitWorkflowService; const orchestrationEngine = yield* OrchestrationEngineService; const projectionSnapshotQuery = yield* ProjectionSnapshotQuery; + const providerRegistry = yield* ProviderRegistry; const runtimeContext = yield* Effect.context(); const runPromise = Effect.runPromiseWith(runtimeContext); @@ -368,6 +494,7 @@ const makeOperations = Effect.gen(function* () { const startTaskEffect = Effect.fn("T3CodeDiscordChannel.startTask")(function* ( prompt: string, config: DiscordChannelSettings, + requestedModelSelection?: ModelSelection, ) { if (config.projectId === null) { return yield* new DiscordChannelTaskError({ @@ -381,7 +508,8 @@ const makeOperations = Effect.gen(function* () { }); } const project = projectOption.value; - if (project.defaultModelSelection === null) { + const modelSelection = requestedModelSelection ?? project.defaultModelSelection; + if (modelSelection === null) { return yield* new DiscordChannelTaskError({ message: "Discord channel project has no default model", }); @@ -400,7 +528,7 @@ const makeOperations = Effect.gen(function* () { threadId, projectId: project.id, title, - modelSelection: project.defaultModelSelection, + modelSelection, runtimeMode: "full-access", interactionMode: "default", branch: worktree?.worktree.refName ?? null, @@ -426,6 +554,7 @@ const makeOperations = Effect.gen(function* () { title, branch: worktree?.worktree.refName ?? null, threadEnvMode: config.threadEnvMode, + modelSelection, state: "queued" as const, }; }); @@ -452,13 +581,17 @@ const makeOperations = Effect.gen(function* () { title: thread.title, branch: thread.branch, threadEnvMode, + modelSelection: thread.modelSelection, state, }; }); return { - startTask: (prompt, config) => runPromise(startTaskEffect(prompt, config)), + startTask: (prompt, config, modelSelection) => + runPromise(startTaskEffect(prompt, config, modelSelection)), getTaskStatus: (threadId) => runPromise(getTaskStatusEffect(threadId)), + listModels: () => + runPromise(providerRegistry.getProviders.pipe(Effect.map(discordModelOptions))), } satisfies T3CodeChannelOperations; }); @@ -498,7 +631,10 @@ export const layer = Layer.effectDiscard( yield* stopActive(); if (!isDiscordChannelConfigured(config)) return; - const created = createT3CodeChannel({ config, operations }); + const models = yield* Effect.tryPromise(() => operations.listModels()).pipe( + Effect.orElseSucceed(() => []), + ); + const created = createT3CodeChannel({ config, operations, models }); const connected = yield* Effect.tryPromise(() => created.channel.ɵruntime.start()).pipe( Effect.timeout("15 seconds"), Effect.as(true), diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f056312ad87..105f1a0ade4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -483,6 +483,9 @@ importers: yaml: specifier: ^2.9.0 version: 2.9.0 + zod: + specifier: ^4.4.3 + version: 4.4.3 devDependencies: '@effect/vitest': specifier: 4.0.0-beta.103 From b97d7fb6cda930b648d57c66a8f1392097c58699 Mon Sep 17 00:00:00 2001 From: Shivam Sharma <91240327+shivamhwp@users.noreply.github.com> Date: Fri, 7 Aug 2026 07:55:55 +0530 Subject: [PATCH 06/12] feat(channels): edit Discord task status in place --- .../src/channels/T3CodeDiscordChannel.test.ts | 19 ++ .../src/channels/T3CodeDiscordChannel.ts | 235 ++++++------------ 2 files changed, 96 insertions(+), 158 deletions(-) diff --git a/apps/server/src/channels/T3CodeDiscordChannel.test.ts b/apps/server/src/channels/T3CodeDiscordChannel.test.ts index 96ee6de4ae6..a6138a9a1ba 100644 --- a/apps/server/src/channels/T3CodeDiscordChannel.test.ts +++ b/apps/server/src/channels/T3CodeDiscordChannel.test.ts @@ -5,6 +5,7 @@ import { ProviderDriverKind, ProviderInstanceId, type ServerProvider, + ThreadId, } from "@t3tools/contracts"; import * as Schema from "effect/Schema"; import { describe } from "vite-plus/test"; @@ -14,6 +15,7 @@ import { discordModelOptions, isDiscordChannelConfigured, resolveDiscordModel, + taskStatusText, } from "./T3CodeDiscordChannel.ts"; const decodeDiscordChannelSettings = Schema.decodeSync(DiscordChannelSettings); @@ -126,4 +128,21 @@ describe("Discord channel model selection", () => { }); expect(resolveDiscordModel(models, "codex/missing")).toBeUndefined(); }); + + it("renders one plain-text status that can be edited as a task progresses", () => { + const task = { + threadId: ThreadId.make("thread-1"), + title: "Fix login", + branch: null, + threadEnvMode: "local", + modelSelection: { + instanceId: ProviderInstanceId.make("codex"), + model: "gpt-5.6-sol", + }, + state: "running", + } as const; + + expect(taskStatusText(task)).toContain("T3 Code is working"); + expect(taskStatusText({ ...task, state: "done" }, 2)).toContain("Changed: 2 files"); + }); }); diff --git a/apps/server/src/channels/T3CodeDiscordChannel.ts b/apps/server/src/channels/T3CodeDiscordChannel.ts index 68aabfee03d..2e27297b56a 100644 --- a/apps/server/src/channels/T3CodeDiscordChannel.ts +++ b/apps/server/src/channels/T3CodeDiscordChannel.ts @@ -1,16 +1,6 @@ import { createChannel, defineChannelCommand } from "@copilotkit/channels-core"; import { discord } from "@copilotkit/channels-discord"; -import { - Actions, - Button, - Context, - Field, - Fields, - Header, - Message, - Section, -} from "@copilotkit/channels-ui"; -import type { Thread } from "@copilotkit/channels-ui"; +import type { MessageRef, Thread } from "@copilotkit/channels-ui"; import { CommandId, type DiscordChannelSettings, @@ -37,9 +27,6 @@ import { ProviderRegistry } from "../provider/Services/ProviderRegistry.ts"; import { forkParked } from "../serverActivation.ts"; import { ServerSettingsService } from "../serverSettings.ts"; -const DISCORD_ACCENT = "#5865f2"; -const COMPLETED_ACCENT = "#22c55e"; -const FAILED_ACCENT = "#ef4444"; const MAX_TITLE_LENGTH = 72; const MAX_BRANCH_SLUG_LENGTH = 40; @@ -106,16 +93,16 @@ export function resolveDiscordModel( return models.find((model) => model.value === value); } -type ChannelThread = Pick & { +type ChannelThread = Pick & { readonly state: () => Promise; readonly setState: (value: unknown) => Promise; }; interface ActiveDiscordChannel { readonly fingerprint: string; - readonly notifyCompleted: (input: { + readonly refreshTask: (input: { readonly threadId: ThreadId; - readonly changedFileCount: number; + readonly changedFileCount?: number; }) => Promise; readonly stop: () => Promise; } @@ -162,19 +149,6 @@ function cleanDiscordPrompt(input: string): string { return input.replace(/<@!?\d+>/gu, "").trim(); } -function taskStateLabel(state: ChannelTaskStatus["state"]): string { - switch (state) { - case "queued": - return "Queued"; - case "running": - return "Running"; - case "done": - return "Done"; - case "failed": - return "Failed"; - } -} - function modelLabel(selection: ModelSelection): string { return `${selection.instanceId}/${selection.model}`; } @@ -194,99 +168,28 @@ function readConversationState(value: unknown): LinkedConversationState { return { ...(t3ThreadId ? { t3ThreadId } : {}), ...(modelSelection ? { modelSelection } : {}) }; } -function statusCard(status: ChannelTaskStatus) { - return Message({ - accent: status.state === "failed" ? FAILED_ACCENT : DISCORD_ACCENT, - fallbackText: `${status.title}: ${taskStateLabel(status.state)}`, - children: [ - Header({ children: status.title }), - Fields({ - children: [ - Field({ label: "Status", children: taskStateLabel(status.state) }), - Field({ label: "Model", children: `\`${modelLabel(status.modelSelection)}\`` }), - Field({ - label: status.threadEnvMode === "worktree" ? "Branch" : "Target", - children: - status.threadEnvMode === "worktree" - ? `\`${status.branch ?? "worktree"}\`` - : "Project checkout", - }), - ], - }), - Context({ - children: - status.threadEnvMode === "worktree" - ? "This task is running in an isolated worktree." - : "This task is running directly in the project's current checkout.", - }), - ], - }); -} - -function startedCard( - task: StartedChannelTask, - onStatus: (thread: Pick) => Promise, -) { - return Message({ - accent: DISCORD_ACCENT, - fallbackText: `T3 Code started: ${task.title}`, - children: [ - Header({ children: "T3 Code task started" }), - Section({ children: task.title }), - Fields({ - children: [ - Field({ label: "Status", children: "Queued" }), - Field({ label: "Model", children: `\`${modelLabel(task.modelSelection)}\`` }), - Field({ - label: task.threadEnvMode === "worktree" ? "Branch" : "Target", - children: - task.threadEnvMode === "worktree" - ? `\`${task.branch ?? "worktree"}\`` - : "Project checkout", - }), - ], - }), - Actions({ - children: Button({ - style: "primary", - value: task.threadId, - onClick: ({ thread }) => onStatus(thread), - children: "Check status", - }), - }), - Context({ children: "T3 Code will reply here when the run and diff are complete." }), - ], - }); -} - -function completedCard(input: { - readonly task: ChannelTaskStatus; - readonly changedFileCount: number; -}) { - const fileLabel = `${input.changedFileCount} changed ${input.changedFileCount === 1 ? "file" : "files"}`; - return Message({ - accent: COMPLETED_ACCENT, - fallbackText: `T3 Code finished: ${input.task.title}`, - children: [ - Header({ children: "T3 Code finished" }), - Section({ children: input.task.title }), - Fields({ - children: [ - Field({ label: "Status", children: "Done" }), - Field({ label: "Diff", children: fileLabel }), - Field({ label: "Model", children: `\`${modelLabel(input.task.modelSelection)}\`` }), - Field({ - label: input.task.threadEnvMode === "worktree" ? "Branch" : "Target", - children: - input.task.threadEnvMode === "worktree" - ? `\`${input.task.branch ?? "worktree"}\`` - : "Project checkout", - }), - ], - }), - Context({ children: "Open T3 Code to inspect the full transcript and diff." }), - ], - }); +export function taskStatusText(task: ChannelTaskStatus, changedFileCount?: number): string { + const heading = (() => { + switch (task.state) { + case "queued": + return "⏳ **T3 Code queued**"; + case "running": + return "🔄 **T3 Code is working**"; + case "done": + return "✅ **T3 Code finished**"; + case "failed": + return "❌ **T3 Code failed**"; + } + })(); + const target = + task.threadEnvMode === "worktree" + ? `Branch: \`${task.branch ?? "worktree"}\`` + : "Target: project checkout"; + const diff = + task.state === "done" && changedFileCount !== undefined + ? `\nChanged: ${changedFileCount} ${changedFileCount === 1 ? "file" : "files"}` + : ""; + return `${heading}\n${task.title}\nModel: \`${modelLabel(task.modelSelection)}\`\n${target}${diff}`; } function createT3CodeChannel(input: { @@ -294,7 +197,14 @@ function createT3CodeChannel(input: { readonly operations: T3CodeChannelOperations; readonly models: ReadonlyArray; }) { - const linkedThreads = new Map>(); + const linkedTasks = new Map< + string, + { + readonly thread: Pick; + messageRef: MessageRef; + changedFileCount?: number; + } + >(); const channel = createChannel({ name: "t3-code", identifyUser: "platform", @@ -309,14 +219,19 @@ function createT3CodeChannel(input: { const postStatus = async (thread: Pick, threadId: ThreadId) => { const status = await input.operations.getTaskStatus(threadId); - await thread.post( - status - ? statusCard(status) - : Message({ - accent: FAILED_ACCENT, - children: Section({ children: "That T3 Code task no longer exists." }), - }), - ); + if (!status) { + await thread.post("That T3 Code task no longer exists."); + return; + } + const linked = linkedTasks.get(threadId); + if (linked) { + linked.messageRef = await linked.thread.update( + linked.messageRef, + taskStatusText(status, linked.changedFileCount), + ); + return; + } + await thread.post(taskStatusText(status)); }; const handleText = async (thread: ChannelThread, rawText: string) => { @@ -338,7 +253,7 @@ function createT3CodeChannel(input: { if (state?.t3ThreadId) { const current = await input.operations.getTaskStatus(ThreadId.make(state.t3ThreadId)); if (current?.state === "queued" || current?.state === "running") { - await thread.post(statusCard(current)); + await postStatus(thread, current.threadId); return; } } @@ -349,23 +264,14 @@ function createT3CodeChannel(input: { ...state, t3ThreadId: task.threadId, } satisfies LinkedConversationState); - linkedThreads.set(task.threadId, thread); - await thread.post(startedCard(task, (target) => postStatus(target, task.threadId))); + const messageRef = await thread.post(taskStatusText(task)); + linkedTasks.set(task.threadId, { thread, messageRef }); + await postStatus(thread, task.threadId); } catch { await thread.post( - Message({ - accent: FAILED_ACCENT, - fallbackText: "T3 Code could not start this task.", - children: [ - Header({ children: "Task did not start" }), - Section({ - children: - input.config.threadEnvMode === "worktree" - ? "T3 Code could not create an isolated worktree. The task was stopped before the agent ran." - : "T3 Code could not start the task in the project checkout. Check the project and provider configuration.", - }), - ], - }), + input.config.threadEnvMode === "worktree" + ? "❌ **T3 Code could not start**\nThe isolated worktree could not be created, so the agent did not run." + : "❌ **T3 Code could not start**\nCheck the project and provider configuration.", ); } }; @@ -438,16 +344,21 @@ function createT3CodeChannel(input: { return { channel, - notifyCompleted: async (completion: { + refreshTask: async (completion: { readonly threadId: ThreadId; - readonly changedFileCount: number; + readonly changedFileCount?: number; }) => { - const thread = linkedThreads.get(completion.threadId); - if (!thread) return; + const linked = linkedTasks.get(completion.threadId); + if (!linked) return; + if (completion.changedFileCount !== undefined) { + linked.changedFileCount = completion.changedFileCount; + } const task = await input.operations.getTaskStatus(completion.threadId); if (!task) return; - await thread.post(completedCard({ task, changedFileCount: completion.changedFileCount })); - linkedThreads.delete(completion.threadId); + linked.messageRef = await linked.thread.update( + linked.messageRef, + taskStatusText(task, linked.changedFileCount), + ); }, }; } @@ -651,7 +562,7 @@ export const layer = Layer.effectDiscard( } yield* Ref.set(activeRef, { fingerprint, - notifyCompleted: created.notifyCompleted, + refreshTask: created.refreshTask, stop: () => created.channel.ɵruntime.stop(), }); }); @@ -668,14 +579,22 @@ export const layer = Layer.effectDiscard( ); yield* forkParked( Stream.runForEach(orchestrationEngine.streamDomainEvents, (event) => { - if (event.type !== "thread.turn-diff-completed") return Effect.void; + if ( + event.type !== "thread.turn-start-requested" && + event.type !== "thread.session-set" && + event.type !== "thread.turn-diff-completed" + ) { + return Effect.void; + } return Ref.get(activeRef).pipe( Effect.flatMap((active) => active ? Effect.tryPromise(() => - active.notifyCompleted({ + active.refreshTask({ threadId: event.payload.threadId, - changedFileCount: event.payload.files.length, + ...(event.type === "thread.turn-diff-completed" + ? { changedFileCount: event.payload.files.length } + : {}), }), ).pipe(Effect.ignoreCause({ log: true })) : Effect.void, From c69fba40913dca6422ad8bd8e7cbb5044843b4c4 Mon Sep 17 00:00:00 2001 From: Shivam Sharma <91240327+shivamhwp@users.noreply.github.com> Date: Fri, 7 Aug 2026 08:17:01 +0530 Subject: [PATCH 07/12] feat(channels): persist the Discord default model --- .../src/channels/T3CodeDiscordChannel.test.ts | 27 +++- .../src/channels/T3CodeDiscordChannel.ts | 149 +++++++++++++----- .../components/settings/ChannelSettings.tsx | 72 ++++++++- packages/contracts/src/settings.ts | 4 + 4 files changed, 210 insertions(+), 42 deletions(-) diff --git a/apps/server/src/channels/T3CodeDiscordChannel.test.ts b/apps/server/src/channels/T3CodeDiscordChannel.test.ts index a6138a9a1ba..8471cfb4374 100644 --- a/apps/server/src/channels/T3CodeDiscordChannel.test.ts +++ b/apps/server/src/channels/T3CodeDiscordChannel.test.ts @@ -12,6 +12,7 @@ import { describe } from "vite-plus/test"; import { channelBranchName, + createDiscordTextClient, discordModelOptions, isDiscordChannelConfigured, resolveDiscordModel, @@ -23,6 +24,7 @@ const decodeDiscordChannelSettings = Schema.decodeSync(DiscordChannelSettings); const configuredDiscord = { enabled: true, projectId: ProjectId.make("project-1"), + modelSelection: null, threadEnvMode: "worktree", baseBranch: "main", branchPrefix: "demo/discord", @@ -34,7 +36,9 @@ const configuredDiscord = { describe("Discord channel isolation", () => { it("keeps isolated worktrees as the default for existing settings", () => { - expect(decodeDiscordChannelSettings({}).threadEnvMode).toBe("worktree"); + const defaults = decodeDiscordChannelSettings({}); + expect(defaults.threadEnvMode).toBe("worktree"); + expect(defaults.modelSelection).toBeNull(); }); it("creates a unique task branch below the configured prefix", () => { @@ -145,4 +149,25 @@ describe("Discord channel model selection", () => { expect(taskStatusText(task)).toContain("T3 Code is working"); expect(taskStatusText({ ...task, state: "done" }, 2)).toContain("Changed: 2 files"); }); + + it("sends and edits native Discord content without Channels UI components", async () => { + const requests: Array<{ readonly url: string; readonly init?: RequestInit }> = []; + const fetchImpl = (async (url: string | URL | Request, init?: RequestInit) => { + requests.push({ url: String(url), ...(init ? { init } : {}) }); + return new Response(JSON.stringify({ id: "message-1" }), { + status: 200, + headers: { "Content-Type": "application/json" }, + }); + }) as typeof fetch; + const client = createDiscordTextClient("secret", fetchImpl); + + const ref = await client.post("channel-1", "Queued"); + await client.update(ref, "Running"); + + expect(requests.map(({ init }) => init?.method)).toEqual(["POST", "PATCH"]); + expect(requests[1]?.url).toContain("/channels/channel-1/messages/message-1"); + expect(JSON.parse(String(requests[0]?.init?.body))).toEqual({ + content: "Queued", + }); + }); }); diff --git a/apps/server/src/channels/T3CodeDiscordChannel.ts b/apps/server/src/channels/T3CodeDiscordChannel.ts index 2e27297b56a..d320be69f59 100644 --- a/apps/server/src/channels/T3CodeDiscordChannel.ts +++ b/apps/server/src/channels/T3CodeDiscordChannel.ts @@ -1,6 +1,5 @@ import { createChannel, defineChannelCommand } from "@copilotkit/channels-core"; import { discord } from "@copilotkit/channels-discord"; -import type { MessageRef, Thread } from "@copilotkit/channels-ui"; import { CommandId, type DiscordChannelSettings, @@ -51,6 +50,7 @@ export interface T3CodeChannelOperations { ) => Promise; readonly getTaskStatus: (threadId: ThreadId) => Promise; readonly listModels: () => Promise>; + readonly setDefaultModel: (modelSelection: ModelSelection) => Promise; } interface LinkedConversationState { @@ -93,11 +93,21 @@ export function resolveDiscordModel( return models.find((model) => model.value === value); } -type ChannelThread = Pick & { +type ChannelThread = { readonly state: () => Promise; readonly setState: (value: unknown) => Promise; }; +interface DiscordTextMessageRef { + readonly channelId: string; + readonly messageId: string; +} + +interface DiscordTextClient { + readonly post: (channelId: string, text: string) => Promise; + readonly update: (ref: DiscordTextMessageRef, text: string) => Promise; +} + interface ActiveDiscordChannel { readonly fingerprint: string; readonly refreshTask: (input: { @@ -172,24 +182,75 @@ export function taskStatusText(task: ChannelTaskStatus, changedFileCount?: numbe const heading = (() => { switch (task.state) { case "queued": - return "⏳ **T3 Code queued**"; + return "⏳ T3 Code queued"; case "running": - return "🔄 **T3 Code is working**"; + return "🔄 T3 Code is working"; case "done": - return "✅ **T3 Code finished**"; + return "✅ T3 Code finished"; case "failed": - return "❌ **T3 Code failed**"; + return "❌ T3 Code failed"; } })(); const target = task.threadEnvMode === "worktree" - ? `Branch: \`${task.branch ?? "worktree"}\`` + ? `Branch: ${task.branch ?? "worktree"}` : "Target: project checkout"; const diff = task.state === "done" && changedFileCount !== undefined ? `\nChanged: ${changedFileCount} ${changedFileCount === 1 ? "file" : "files"}` : ""; - return `${heading}\n${task.title}\nModel: \`${modelLabel(task.modelSelection)}\`\n${target}${diff}`; + return `${heading}\n${task.title}\nModel: ${modelLabel(task.modelSelection)}\n${target}${diff}`; +} + +export function createDiscordTextClient( + botToken: string, + fetchImpl: typeof fetch = fetch, +): DiscordTextClient { + const request = async (url: string, method: "POST" | "PATCH", text: string) => { + const response = await fetchImpl(url, { + method, + headers: { + Authorization: `Bot ${botToken}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ content: text.slice(0, 2_000) }), + }); + if (!response.ok) { + throw new Error(`Discord text message request failed (${response.status})`); + } + return response; + }; + + return { + async post(channelId, text) { + const response = await request( + `https://discord.com/api/v10/channels/${channelId}/messages`, + "POST", + text, + ); + const payload = (await response.json()) as { readonly id?: unknown }; + if (typeof payload.id !== "string") { + throw new Error("Discord text message response did not include a message id"); + } + return { channelId, messageId: payload.id }; + }, + async update(ref, text) { + await request( + `https://discord.com/api/v10/channels/${ref.channelId}/messages/${ref.messageId}`, + "PATCH", + text, + ); + }, + }; +} + +function discordChannelId(thread: ChannelThread): string { + const conversationKey = (thread as ChannelThread & { readonly conversationKey?: unknown }) + .conversationKey; + if (typeof conversationKey !== "string" || conversationKey.length === 0) { + throw new Error("Discord channel thread did not include a conversation key"); + } + return conversationKey; } function createT3CodeChannel(input: { @@ -197,11 +258,11 @@ function createT3CodeChannel(input: { readonly operations: T3CodeChannelOperations; readonly models: ReadonlyArray; }) { + const textClient = createDiscordTextClient(input.config.botToken); const linkedTasks = new Map< string, { - readonly thread: Pick; - messageRef: MessageRef; + readonly messageRef: DiscordTextMessageRef; changedFileCount?: number; } >(); @@ -217,21 +278,21 @@ function createT3CodeChannel(input: { ], }); - const postStatus = async (thread: Pick, threadId: ThreadId) => { + const postText = (thread: ChannelThread, text: string) => + textClient.post(discordChannelId(thread), text); + + const postStatus = async (thread: ChannelThread, threadId: ThreadId) => { const status = await input.operations.getTaskStatus(threadId); if (!status) { - await thread.post("That T3 Code task no longer exists."); + await postText(thread, "That T3 Code task no longer exists."); return; } const linked = linkedTasks.get(threadId); if (linked) { - linked.messageRef = await linked.thread.update( - linked.messageRef, - taskStatusText(status, linked.changedFileCount), - ); + await textClient.update(linked.messageRef, taskStatusText(status, linked.changedFileCount)); return; } - await thread.post(taskStatusText(status)); + await postText(thread, taskStatusText(status)); }; const handleText = async (thread: ChannelThread, rawText: string) => { @@ -239,14 +300,17 @@ function createT3CodeChannel(input: { const state = readConversationState(await thread.state()); if (text.toLocaleLowerCase() === "status") { if (!state?.t3ThreadId) { - await thread.post("No T3 Code task is linked to this Discord thread yet."); + await postText(thread, "No T3 Code task is linked to this Discord thread yet."); return; } await postStatus(thread, ThreadId.make(state.t3ThreadId)); return; } if (text.length === 0) { - await thread.post("Mention me with a coding task, or send `status` to check the linked run."); + await postText( + thread, + "Mention me with a coding task, or send `status` to check the linked run.", + ); return; } @@ -264,14 +328,15 @@ function createT3CodeChannel(input: { ...state, t3ThreadId: task.threadId, } satisfies LinkedConversationState); - const messageRef = await thread.post(taskStatusText(task)); - linkedTasks.set(task.threadId, { thread, messageRef }); + const messageRef = await postText(thread, taskStatusText(task)); + linkedTasks.set(task.threadId, { messageRef }); await postStatus(thread, task.threadId); } catch { - await thread.post( + await postText( + thread, input.config.threadEnvMode === "worktree" - ? "❌ **T3 Code could not start**\nThe isolated worktree could not be created, so the agent did not run." - : "❌ **T3 Code could not start**\nCheck the project and provider configuration.", + ? "❌ T3 Code could not start\nThe isolated worktree could not be created, so the agent did not run." + : "❌ T3 Code could not start\nCheck the project and provider configuration.", ); } }; @@ -309,7 +374,8 @@ function createT3CodeChannel(input: { async handler({ thread, options }) { const selected = resolveDiscordModel(selectableModels, options.model); if (!selected) { - await thread.post( + await postText( + thread, "That model is not currently available. Run `/models` to see the list.", ); return; @@ -319,9 +385,8 @@ function createT3CodeChannel(input: { ...state, modelSelection: selected.selection, } satisfies LinkedConversationState); - await thread.post( - `Future T3 Code tasks in this channel will use **${selected.label}** (\`${selected.value}\`).`, - ); + await postText(thread, `Default model saved: ${selected.label} (${selected.value}).`); + await input.operations.setDefaultModel(selected.selection); }, }), ); @@ -331,12 +396,12 @@ function createT3CodeChannel(input: { description: "List models available to T3 Code and show the current selection.", async handler({ thread }) { const state = readConversationState(await thread.state()); - const current = state.modelSelection ? modelLabel(state.modelSelection) : "project default"; - const list = selectableModels - .map(({ value, label }) => `• \`${value}\` — ${label}`) - .join("\n"); - await thread.post( - `**Current model:** ${current === "project default" ? current : `\`${current}\``}\n\n${list || "No runnable models are currently available."}`, + const effectiveSelection = state.modelSelection ?? input.config.modelSelection; + const current = effectiveSelection ? modelLabel(effectiveSelection) : "project default"; + const list = selectableModels.map(({ value, label }) => `- ${value} — ${label}`).join("\n"); + await postText( + thread, + `Current model: ${current}\n\n${list || "No runnable models are currently available."}`, ); }, }), @@ -355,10 +420,7 @@ function createT3CodeChannel(input: { } const task = await input.operations.getTaskStatus(completion.threadId); if (!task) return; - linked.messageRef = await linked.thread.update( - linked.messageRef, - taskStatusText(task, linked.changedFileCount), - ); + await textClient.update(linked.messageRef, taskStatusText(task, linked.changedFileCount)); }, }; } @@ -369,6 +431,7 @@ const makeOperations = Effect.gen(function* () { const orchestrationEngine = yield* OrchestrationEngineService; const projectionSnapshotQuery = yield* ProjectionSnapshotQuery; const providerRegistry = yield* ProviderRegistry; + const settingsService = yield* ServerSettingsService; const runtimeContext = yield* Effect.context(); const runPromise = Effect.runPromiseWith(runtimeContext); @@ -419,7 +482,8 @@ const makeOperations = Effect.gen(function* () { }); } const project = projectOption.value; - const modelSelection = requestedModelSelection ?? project.defaultModelSelection; + const modelSelection = + requestedModelSelection ?? config.modelSelection ?? project.defaultModelSelection; if (modelSelection === null) { return yield* new DiscordChannelTaskError({ message: "Discord channel project has no default model", @@ -503,6 +567,12 @@ const makeOperations = Effect.gen(function* () { getTaskStatus: (threadId) => runPromise(getTaskStatusEffect(threadId)), listModels: () => runPromise(providerRegistry.getProviders.pipe(Effect.map(discordModelOptions))), + setDefaultModel: (modelSelection) => + runPromise( + settingsService + .updateSettings({ channelIntegrations: { discord: { modelSelection } } }) + .pipe(Effect.asVoid), + ), } satisfies T3CodeChannelOperations; }); @@ -510,6 +580,7 @@ function configFingerprint(config: DiscordChannelSettings): string { return [ config.enabled, config.projectId, + config.modelSelection ? modelLabel(config.modelSelection) : "", config.threadEnvMode, config.baseBranch, config.branchPrefix, diff --git a/apps/web/src/components/settings/ChannelSettings.tsx b/apps/web/src/components/settings/ChannelSettings.tsx index 674a4b7a919..e1be1f0a9d9 100644 --- a/apps/web/src/components/settings/ChannelSettings.tsx +++ b/apps/web/src/components/settings/ChannelSettings.tsx @@ -1,11 +1,24 @@ +import { useAtomValue } from "@effect/atom-react"; import { BotIcon, ExternalLinkIcon, GitBranchIcon, ShieldCheckIcon } from "lucide-react"; -import { ProjectId, type ServerSettingsPatch } from "@t3tools/contracts"; +import { type ModelSelection, ProjectId, type ServerSettingsPatch } from "@t3tools/contracts"; +import { createModelSelection } from "@t3tools/shared/model"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import { usePersistPrimaryServerSettings, usePrimarySettings } from "../../hooks/useSettings"; import { ensureLocalApi } from "../../localApi"; +import { + getCustomModelOptionsByInstance, + resolveAppModelSelectionState, +} from "../../modelSelection"; +import { + applyProviderInstanceSettings, + deriveProviderInstanceEntries, + sortProviderInstanceEntries, +} from "../../providerInstances"; import { useProjects } from "../../state/entities"; import { usePrimaryEnvironment } from "../../state/environments"; +import { primaryServerProvidersAtom } from "../../state/server"; +import { ProviderModelPicker } from "../chat/ProviderModelPicker"; import { Badge } from "../ui/badge"; import { Button } from "../ui/button"; import { Input } from "../ui/input"; @@ -49,10 +62,12 @@ type SaveStatus = "idle" | "unsaved" | "saving" | "saved" | "error"; type DirtyField = "applicationId" | "guildId" | "botToken" | "baseBranch" | "branchPrefix"; export function ChannelSettings() { - const settings = usePrimarySettings((value) => value.channelIntegrations.discord); + const allSettings = usePrimarySettings(); + const settings = allSettings.channelIntegrations.discord; const persistServerSettings = usePersistPrimaryServerSettings(); const primaryEnvironment = usePrimaryEnvironment(); const allProjects = useProjects(); + const serverProviders = useAtomValue(primaryServerProvidersAtom); const projects = useMemo( () => primaryEnvironment @@ -64,6 +79,9 @@ export function ChannelSettings() { ); const [enabled, setEnabled] = useState(settings.enabled); const [projectId, setProjectId] = useState(settings.projectId); + const [modelSelection, setModelSelection] = useState( + settings.modelSelection, + ); const [threadEnvMode, setThreadEnvMode] = useState(settings.threadEnvMode); const [baseBranch, setBaseBranch] = useState(settings.baseBranch); const [branchPrefix, setBranchPrefix] = useState(settings.branchPrefix); @@ -78,6 +96,7 @@ export function ChannelSettings() { useEffect(() => { setEnabled(settings.enabled); setProjectId(settings.projectId); + setModelSelection(settings.modelSelection); setThreadEnvMode(settings.threadEnvMode); if (!dirtyFieldsRef.current.has("baseBranch")) setBaseBranch(settings.baseBranch); if (!dirtyFieldsRef.current.has("branchPrefix")) setBranchPrefix(settings.branchPrefix); @@ -96,6 +115,31 @@ export function ChannelSettings() { applicationId.trim().length > 0 && hasBotToken; const selectedProject = projects.find((project) => project.id === projectId) ?? null; + const fallbackModelSelection = useMemo( + () => resolveAppModelSelectionState(allSettings, serverProviders), + [allSettings, serverProviders], + ); + const activeModelSelection = useMemo( + () => modelSelection ?? selectedProject?.defaultModelSelection ?? fallbackModelSelection, + [fallbackModelSelection, modelSelection, selectedProject?.defaultModelSelection], + ); + const instanceEntries = useMemo( + () => + sortProviderInstanceEntries( + applyProviderInstanceSettings(deriveProviderInstanceEntries(serverProviders), allSettings), + ), + [allSettings, serverProviders], + ); + const modelOptionsByInstance = useMemo( + () => + getCustomModelOptionsByInstance( + allSettings, + serverProviders, + activeModelSelection.instanceId, + activeModelSelection.model, + ), + [activeModelSelection.instanceId, activeModelSelection.model, allSettings, serverProviders], + ); const discordInstallUrl = buildDiscordInstallUrl(applicationId, guildId); const persistDiscordPatch = useCallback( @@ -116,6 +160,7 @@ export function ChannelSettings() { { enabled, projectId, + modelSelection, threadEnvMode, baseBranch, branchPrefix, @@ -139,6 +184,7 @@ export function ChannelSettings() { branchPrefix, enabled, guildId, + modelSelection, persistDiscordPatch, projectId, threadEnvMode, @@ -199,6 +245,28 @@ export function ChannelSettings() { } /> + { + const next = createModelSelection(instanceId, model); + setModelSelection(next); + void persistDiscordPatch({ modelSelection: next }); + }} + /> + } + /> Date: Fri, 7 Aug 2026 08:23:43 +0530 Subject: [PATCH 08/12] fix(channels): return Discord task responses --- .../src/channels/T3CodeDiscordChannel.test.ts | 32 +++++- .../src/channels/T3CodeDiscordChannel.ts | 105 +++++++++--------- 2 files changed, 82 insertions(+), 55 deletions(-) diff --git a/apps/server/src/channels/T3CodeDiscordChannel.test.ts b/apps/server/src/channels/T3CodeDiscordChannel.test.ts index 8471cfb4374..f7adbcbe097 100644 --- a/apps/server/src/channels/T3CodeDiscordChannel.test.ts +++ b/apps/server/src/channels/T3CodeDiscordChannel.test.ts @@ -11,6 +11,7 @@ import * as Schema from "effect/Schema"; import { describe } from "vite-plus/test"; import { + assistantResponseText, channelBranchName, createDiscordTextClient, discordModelOptions, @@ -144,10 +145,37 @@ describe("Discord channel model selection", () => { model: "gpt-5.6-sol", }, state: "running", + assistantResponse: null, } as const; - expect(taskStatusText(task)).toContain("T3 Code is working"); - expect(taskStatusText({ ...task, state: "done" }, 2)).toContain("Changed: 2 files"); + expect(taskStatusText(task)).toBe("🔄 Fix login"); + expect( + taskStatusText({ + ...task, + state: "done", + assistantResponse: "Fixed the login flow and added coverage.", + }), + ).toBe("✅ Fix login\n\nFixed the login flow and added coverage."); + expect(taskStatusText(task)).not.toContain("Model:"); + expect(taskStatusText(task)).not.toContain("Target:"); + }); + + it("uses the final assistant message as the completed Discord response", () => { + expect( + assistantResponseText({ + assistantMessageId: "assistant-final", + turnId: "turn-1", + messages: [ + { id: "assistant-progress", role: "assistant", text: "Working", turnId: "turn-1" }, + { + id: "assistant-final", + role: "assistant", + text: " The requested change is complete. ", + turnId: "turn-1", + }, + ], + }), + ).toBe("The requested change is complete."); }); it("sends and edits native Discord content without Channels UI components", async () => { diff --git a/apps/server/src/channels/T3CodeDiscordChannel.ts b/apps/server/src/channels/T3CodeDiscordChannel.ts index d320be69f59..814952207b9 100644 --- a/apps/server/src/channels/T3CodeDiscordChannel.ts +++ b/apps/server/src/channels/T3CodeDiscordChannel.ts @@ -36,6 +36,7 @@ export interface ChannelTaskStatus { readonly threadEnvMode: "local" | "worktree"; readonly modelSelection: ModelSelection; readonly state: "queued" | "running" | "done" | "failed"; + readonly assistantResponse: string | null; } export interface StartedChannelTask extends ChannelTaskStatus { @@ -110,10 +111,7 @@ interface DiscordTextClient { interface ActiveDiscordChannel { readonly fingerprint: string; - readonly refreshTask: (input: { - readonly threadId: ThreadId; - readonly changedFileCount?: number; - }) => Promise; + readonly refreshTask: (threadId: ThreadId) => Promise; readonly stop: () => Promise; } @@ -178,28 +176,39 @@ function readConversationState(value: unknown): LinkedConversationState { return { ...(t3ThreadId ? { t3ThreadId } : {}), ...(modelSelection ? { modelSelection } : {}) }; } -export function taskStatusText(task: ChannelTaskStatus, changedFileCount?: number): string { - const heading = (() => { - switch (task.state) { - case "queued": - return "⏳ T3 Code queued"; - case "running": - return "🔄 T3 Code is working"; - case "done": - return "✅ T3 Code finished"; - case "failed": - return "❌ T3 Code failed"; - } - })(); - const target = - task.threadEnvMode === "worktree" - ? `Branch: ${task.branch ?? "worktree"}` - : "Target: project checkout"; - const diff = - task.state === "done" && changedFileCount !== undefined - ? `\nChanged: ${changedFileCount} ${changedFileCount === 1 ? "file" : "files"}` - : ""; - return `${heading}\n${task.title}\nModel: ${modelLabel(task.modelSelection)}\n${target}${diff}`; +export function taskStatusText(task: ChannelTaskStatus): string { + switch (task.state) { + case "queued": + return `⏳ ${task.title}`; + case "running": + return `🔄 ${task.title}`; + case "done": + return `✅ ${task.title}\n\n${task.assistantResponse ?? "Done."}`; + case "failed": + return `❌ ${task.title}\n\nTask failed.`; + } +} + +export function assistantResponseText(input: { + readonly assistantMessageId: string | null | undefined; + readonly turnId: string | null | undefined; + readonly messages: ReadonlyArray<{ + readonly id: string; + readonly role: string; + readonly text: string; + readonly turnId: string | null; + }>; +}): string | null { + const exactMessage = input.assistantMessageId + ? input.messages.find((message) => message.id === input.assistantMessageId) + : undefined; + const fallbackMessage = input.turnId + ? input.messages.findLast( + (message) => message.role === "assistant" && message.turnId === input.turnId, + ) + : undefined; + const text = (exactMessage ?? fallbackMessage)?.text.trim(); + return text && text.length > 0 ? text : null; } export function createDiscordTextClient( @@ -259,13 +268,7 @@ function createT3CodeChannel(input: { readonly models: ReadonlyArray; }) { const textClient = createDiscordTextClient(input.config.botToken); - const linkedTasks = new Map< - string, - { - readonly messageRef: DiscordTextMessageRef; - changedFileCount?: number; - } - >(); + const linkedTasks = new Map(); const channel = createChannel({ name: "t3-code", identifyUser: "platform", @@ -289,7 +292,7 @@ function createT3CodeChannel(input: { } const linked = linkedTasks.get(threadId); if (linked) { - await textClient.update(linked.messageRef, taskStatusText(status, linked.changedFileCount)); + await textClient.update(linked.messageRef, taskStatusText(status)); return; } await postText(thread, taskStatusText(status)); @@ -409,18 +412,12 @@ function createT3CodeChannel(input: { return { channel, - refreshTask: async (completion: { - readonly threadId: ThreadId; - readonly changedFileCount?: number; - }) => { - const linked = linkedTasks.get(completion.threadId); + refreshTask: async (threadId: ThreadId) => { + const linked = linkedTasks.get(threadId); if (!linked) return; - if (completion.changedFileCount !== undefined) { - linked.changedFileCount = completion.changedFileCount; - } - const task = await input.operations.getTaskStatus(completion.threadId); + const task = await input.operations.getTaskStatus(threadId); if (!task) return; - await textClient.update(linked.messageRef, taskStatusText(task, linked.changedFileCount)); + await textClient.update(linked.messageRef, taskStatusText(task)); }, }; } @@ -531,13 +528,14 @@ const makeOperations = Effect.gen(function* () { threadEnvMode: config.threadEnvMode, modelSelection, state: "queued" as const, + assistantResponse: null, }; }); const getTaskStatusEffect = Effect.fn("T3CodeDiscordChannel.getTaskStatus")(function* ( threadId: ThreadId, ) { - const threadOption = yield* projectionSnapshotQuery.getThreadShellById(threadId); + const threadOption = yield* projectionSnapshotQuery.getThreadDetailById(threadId); if (Option.isNone(threadOption)) return null; const thread = threadOption.value; const state = (() => { @@ -551,6 +549,11 @@ const makeOperations = Effect.gen(function* () { return "queued" as const; })(); const threadEnvMode = thread.worktreePath === null ? ("local" as const) : ("worktree" as const); + const assistantResponse = assistantResponseText({ + assistantMessageId: thread.latestTurn?.assistantMessageId, + turnId: thread.latestTurn?.turnId, + messages: thread.messages, + }); return { threadId, title: thread.title, @@ -558,6 +561,7 @@ const makeOperations = Effect.gen(function* () { threadEnvMode, modelSelection: thread.modelSelection, state, + assistantResponse, }; }); @@ -660,14 +664,9 @@ export const layer = Layer.effectDiscard( return Ref.get(activeRef).pipe( Effect.flatMap((active) => active - ? Effect.tryPromise(() => - active.refreshTask({ - threadId: event.payload.threadId, - ...(event.type === "thread.turn-diff-completed" - ? { changedFileCount: event.payload.files.length } - : {}), - }), - ).pipe(Effect.ignoreCause({ log: true })) + ? Effect.tryPromise(() => active.refreshTask(event.payload.threadId)).pipe( + Effect.ignoreCause({ log: true }), + ) : Effect.void, ), ); From fc9d8de6e754a8c3092986c846916a35db38d3e2 Mon Sep 17 00:00:00 2001 From: Shivam Sharma <91240327+shivamhwp@users.noreply.github.com> Date: Fri, 7 Aug 2026 08:41:21 +0530 Subject: [PATCH 09/12] fix(channels): reliably finish Discord tasks --- .../src/channels/T3CodeDiscordChannel.test.ts | 38 ++++-- .../src/channels/T3CodeDiscordChannel.ts | 122 ++++++++++++++---- .../settings/discordInstallUrl.test.ts | 2 +- .../components/settings/discordInstallUrl.ts | 2 +- 4 files changed, 125 insertions(+), 39 deletions(-) diff --git a/apps/server/src/channels/T3CodeDiscordChannel.test.ts b/apps/server/src/channels/T3CodeDiscordChannel.test.ts index f7adbcbe097..d5bc8d40af5 100644 --- a/apps/server/src/channels/T3CodeDiscordChannel.test.ts +++ b/apps/server/src/channels/T3CodeDiscordChannel.test.ts @@ -148,14 +148,14 @@ describe("Discord channel model selection", () => { assistantResponse: null, } as const; - expect(taskStatusText(task)).toBe("🔄 Fix login"); + expect(taskStatusText(task)).toBe("Fix login 🔄"); expect( taskStatusText({ ...task, state: "done", assistantResponse: "Fixed the login flow and added coverage.", }), - ).toBe("✅ Fix login\n\nFixed the login flow and added coverage."); + ).toBe("Fix login ✅\n\nFixed the login flow and added coverage."); expect(taskStatusText(task)).not.toContain("Model:"); expect(taskStatusText(task)).not.toContain("Target:"); }); @@ -181,21 +181,41 @@ describe("Discord channel model selection", () => { it("sends and edits native Discord content without Channels UI components", async () => { const requests: Array<{ readonly url: string; readonly init?: RequestInit }> = []; const fetchImpl = (async (url: string | URL | Request, init?: RequestInit) => { - requests.push({ url: String(url), ...(init ? { init } : {}) }); - return new Response(JSON.stringify({ id: "message-1" }), { - status: 200, - headers: { "Content-Type": "application/json" }, - }); + const requestUrl = String(url); + requests.push({ url: requestUrl, ...(init ? { init } : {}) }); + return new Response( + JSON.stringify( + requestUrl.endsWith("/users/@me") && init?.method === "GET" + ? { username: "copilotkit-ad" } + : { id: "message-1" }, + ), + { + status: 200, + headers: { "Content-Type": "application/json" }, + }, + ); }) as typeof fetch; const client = createDiscordTextClient("secret", fetchImpl); const ref = await client.post("channel-1", "Queued"); await client.update(ref, "Running"); - - expect(requests.map(({ init }) => init?.method)).toEqual(["POST", "PATCH"]); + await client.ensureBotUsername("copilot"); + await client.setGuildNickname("guild-1", "copilot"); + + expect(requests.map(({ init }) => init?.method)).toEqual([ + "POST", + "PATCH", + "GET", + "PATCH", + "PATCH", + ]); expect(requests[1]?.url).toContain("/channels/channel-1/messages/message-1"); + expect(requests[3]?.url).toContain("/users/@me"); + expect(requests[4]?.url).toContain("/guilds/guild-1/members/@me"); expect(JSON.parse(String(requests[0]?.init?.body))).toEqual({ content: "Queued", }); + expect(JSON.parse(String(requests[3]?.init?.body))).toEqual({ username: "copilot" }); + expect(JSON.parse(String(requests[4]?.init?.body))).toEqual({ nick: "copilot" }); }); }); diff --git a/apps/server/src/channels/T3CodeDiscordChannel.ts b/apps/server/src/channels/T3CodeDiscordChannel.ts index 814952207b9..81fa369c926 100644 --- a/apps/server/src/channels/T3CodeDiscordChannel.ts +++ b/apps/server/src/channels/T3CodeDiscordChannel.ts @@ -15,6 +15,7 @@ import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Option from "effect/Option"; import * as Ref from "effect/Ref"; +import * as Schedule from "effect/Schedule"; import * as Schema from "effect/Schema"; import * as Stream from "effect/Stream"; import { z } from "zod"; @@ -107,11 +108,14 @@ interface DiscordTextMessageRef { interface DiscordTextClient { readonly post: (channelId: string, text: string) => Promise; readonly update: (ref: DiscordTextMessageRef, text: string) => Promise; + readonly ensureBotUsername: (username: string) => Promise; + readonly setGuildNickname: (guildId: string, nickname: string) => Promise; } interface ActiveDiscordChannel { readonly fingerprint: string; readonly refreshTask: (threadId: ThreadId) => Promise; + readonly refreshPendingTasks: () => Promise; readonly stop: () => Promise; } @@ -179,13 +183,13 @@ function readConversationState(value: unknown): LinkedConversationState { export function taskStatusText(task: ChannelTaskStatus): string { switch (task.state) { case "queued": - return `⏳ ${task.title}`; + return `${task.title} ⏳`; case "running": - return `🔄 ${task.title}`; + return `${task.title} 🔄`; case "done": - return `✅ ${task.title}\n\n${task.assistantResponse ?? "Done."}`; + return `${task.title} ✅\n\n${task.assistantResponse ?? "Done."}`; case "failed": - return `❌ ${task.title}\n\nTask failed.`; + return `${task.title} ❌\n\nTask failed.`; } } @@ -215,17 +219,17 @@ export function createDiscordTextClient( botToken: string, fetchImpl: typeof fetch = fetch, ): DiscordTextClient { - const request = async (url: string, method: "POST" | "PATCH", text: string) => { + const request = async (url: string, method: "GET" | "POST" | "PATCH", body?: unknown) => { const response = await fetchImpl(url, { method, headers: { Authorization: `Bot ${botToken}`, "Content-Type": "application/json", }, - body: JSON.stringify({ content: text.slice(0, 2_000) }), + ...(body === undefined ? {} : { body: JSON.stringify(body) }), }); if (!response.ok) { - throw new Error(`Discord text message request failed (${response.status})`); + throw new Error(`Discord API request failed (${response.status})`); } return response; }; @@ -235,7 +239,7 @@ export function createDiscordTextClient( const response = await request( `https://discord.com/api/v10/channels/${channelId}/messages`, "POST", - text, + { content: text.slice(0, 2_000) }, ); const payload = (await response.json()) as { readonly id?: unknown }; if (typeof payload.id !== "string") { @@ -247,9 +251,20 @@ export function createDiscordTextClient( await request( `https://discord.com/api/v10/channels/${ref.channelId}/messages/${ref.messageId}`, "PATCH", - text, + { content: text.slice(0, 2_000) }, ); }, + async ensureBotUsername(username) { + const current = await request("https://discord.com/api/v10/users/@me", "GET"); + const user = (await current.json()) as { readonly username?: unknown }; + if (user.username === username) return; + await request("https://discord.com/api/v10/users/@me", "PATCH", { username }); + }, + async setGuildNickname(guildId, nickname) { + await request(`https://discord.com/api/v10/guilds/${guildId}/members/@me`, "PATCH", { + nick: nickname, + }); + }, }; } @@ -268,7 +283,14 @@ function createT3CodeChannel(input: { readonly models: ReadonlyArray; }) { const textClient = createDiscordTextClient(input.config.botToken); - const linkedTasks = new Map(); + const linkedTasks = new Map< + string, + { + readonly messageRef: DiscordTextMessageRef; + lastText: string; + terminal: boolean; + } + >(); const channel = createChannel({ name: "t3-code", identifyUser: "platform", @@ -284,6 +306,18 @@ function createT3CodeChannel(input: { const postText = (thread: ChannelThread, text: string) => textClient.post(discordChannelId(thread), text); + const updateLinkedTask = async (threadId: ThreadId, status: ChannelTaskStatus) => { + const linked = linkedTasks.get(threadId); + if (!linked) return; + const nextText = taskStatusText(status); + if (nextText !== linked.lastText) { + await textClient.update(linked.messageRef, nextText); + linked.lastText = nextText; + } + linked.terminal = + status.state === "failed" || (status.state === "done" && status.assistantResponse !== null); + }; + const postStatus = async (thread: ChannelThread, threadId: ThreadId) => { const status = await input.operations.getTaskStatus(threadId); if (!status) { @@ -292,7 +326,7 @@ function createT3CodeChannel(input: { } const linked = linkedTasks.get(threadId); if (linked) { - await textClient.update(linked.messageRef, taskStatusText(status)); + await updateLinkedTask(threadId, status); return; } await postText(thread, taskStatusText(status)); @@ -310,10 +344,7 @@ function createT3CodeChannel(input: { return; } if (text.length === 0) { - await postText( - thread, - "Mention me with a coding task, or send `status` to check the linked run.", - ); + await postText(thread, "Use `/t3` with a coding task."); return; } @@ -331,8 +362,9 @@ function createT3CodeChannel(input: { ...state, t3ThreadId: task.threadId, } satisfies LinkedConversationState); - const messageRef = await postText(thread, taskStatusText(task)); - linkedTasks.set(task.threadId, { messageRef }); + const initialText = taskStatusText(task); + const messageRef = await postText(thread, initialText); + linkedTasks.set(task.threadId, { messageRef, lastText: initialText, terminal: false }); await postStatus(thread, task.threadId); } catch { await postText( @@ -344,7 +376,6 @@ function createT3CodeChannel(input: { } }; - channel.onMention(({ thread, message }) => handleText(thread, message.text)); channel.onCommand( defineChannelCommand({ name: "t3", @@ -410,15 +441,35 @@ function createT3CodeChannel(input: { }), ); + const refreshTask = async (threadId: ThreadId) => { + const linked = linkedTasks.get(threadId); + if (!linked || linked.terminal) return; + const task = await input.operations.getTaskStatus(threadId); + if (!task) return; + await updateLinkedTask(threadId, task); + }; + return { channel, - refreshTask: async (threadId: ThreadId) => { - const linked = linkedTasks.get(threadId); - if (!linked) return; - const task = await input.operations.getTaskStatus(threadId); - if (!task) return; - await textClient.update(linked.messageRef, taskStatusText(task)); + refreshTask, + refreshPendingTasks: async () => { + await Promise.all( + Array.from(linkedTasks) + .filter(([, linked]) => !linked.terminal) + .map(([threadId]) => refreshTask(ThreadId.make(threadId))), + ); }, + setDisplayName: async () => { + const updates = [textClient.ensureBotUsername("copilot")]; + if (input.config.guildId.length > 0) { + updates.push(textClient.setGuildNickname(input.config.guildId, "copilot")); + } + const results = await Promise.allSettled(updates); + if (results.some((result) => result.status === "fulfilled")) return; + const failure = results.find((result) => result.status === "rejected"); + throw failure?.reason ?? new Error("Discord bot display name could not be updated"); + }, + stop: () => channel.ɵruntime.stop(), }; } @@ -630,15 +681,18 @@ export const layer = Layer.effectDiscard( Effect.catchCause(() => Effect.succeed(false)), ); if (!connected) { - yield* Effect.tryPromise(() => created.channel.ɵruntime.stop()).pipe( - Effect.ignoreCause({ log: true }), - ); + yield* Effect.tryPromise(() => created.stop()).pipe(Effect.ignoreCause({ log: true })); return; } + yield* Effect.tryPromise(() => created.setDisplayName()).pipe( + Effect.timeout("5 seconds"), + Effect.ignoreCause({ log: true }), + ); yield* Ref.set(activeRef, { fingerprint, refreshTask: created.refreshTask, - stop: () => created.channel.ɵruntime.stop(), + refreshPendingTasks: created.refreshPendingTasks, + stop: created.stop, }); }); @@ -672,5 +726,17 @@ export const layer = Layer.effectDiscard( ); }), ); + yield* forkParked( + Ref.get(activeRef).pipe( + Effect.flatMap((active) => + active + ? Effect.tryPromise(() => active.refreshPendingTasks()).pipe( + Effect.ignoreCause({ log: true }), + ) + : Effect.void, + ), + Effect.repeat(Schedule.spaced("1 second")), + ), + ); }), ); diff --git a/apps/web/src/components/settings/discordInstallUrl.test.ts b/apps/web/src/components/settings/discordInstallUrl.test.ts index a69140cfd8e..389d83fe42c 100644 --- a/apps/web/src/components/settings/discordInstallUrl.test.ts +++ b/apps/web/src/components/settings/discordInstallUrl.test.ts @@ -12,7 +12,7 @@ describe("buildDiscordInstallUrl", () => { expect(Object.fromEntries(url.searchParams)).toEqual({ client_id: "1535085613399933028", integration_type: "0", - permissions: "309237713920", + permissions: "309304822784", scope: "bot applications.commands", }); }); diff --git a/apps/web/src/components/settings/discordInstallUrl.ts b/apps/web/src/components/settings/discordInstallUrl.ts index 225fcd94a38..19aa2e524a5 100644 --- a/apps/web/src/components/settings/discordInstallUrl.ts +++ b/apps/web/src/components/settings/discordInstallUrl.ts @@ -1,4 +1,4 @@ -const DISCORD_CHANNEL_PERMISSIONS = "309237713920"; +const DISCORD_CHANNEL_PERMISSIONS = "309304822784"; export function buildDiscordInstallUrl(applicationId: string, guildId: string): string | null { const clientId = applicationId.trim(); From 383d3a252468c902dd0a658ee68303247c028173 Mon Sep 17 00:00:00 2001 From: Shivam Sharma <91240327+shivamhwp@users.noreply.github.com> Date: Fri, 7 Aug 2026 09:15:07 +0530 Subject: [PATCH 10/12] fix(channels): deliver final T3 responses --- .../src/channels/T3CodeDiscordChannel.test.ts | 54 +++-- .../src/channels/T3CodeDiscordChannel.ts | 222 ++++++++++++------ 2 files changed, 185 insertions(+), 91 deletions(-) diff --git a/apps/server/src/channels/T3CodeDiscordChannel.test.ts b/apps/server/src/channels/T3CodeDiscordChannel.test.ts index d5bc8d40af5..638cb74eb54 100644 --- a/apps/server/src/channels/T3CodeDiscordChannel.test.ts +++ b/apps/server/src/channels/T3CodeDiscordChannel.test.ts @@ -9,15 +9,18 @@ import { } from "@t3tools/contracts"; import * as Schema from "effect/Schema"; import { describe } from "vite-plus/test"; +import { renderToIR } from "@copilotkit/channels-ui"; import { assistantResponseText, channelBranchName, - createDiscordTextClient, + createDiscordProfileClient, discordModelOptions, + isDeliverableTaskResponse, isDiscordChannelConfigured, resolveDiscordModel, taskStatusText, + taskStatusUi, } from "./T3CodeDiscordChannel.ts"; const decodeDiscordChannelSettings = Schema.decodeSync(DiscordChannelSettings); @@ -134,7 +137,7 @@ describe("Discord channel model selection", () => { expect(resolveDiscordModel(models, "codex/missing")).toBeUndefined(); }); - it("renders one plain-text status that can be edited as a task progresses", () => { + it("renders a minimal CopilotKit status that can be edited as a task progresses", () => { const task = { threadId: ThreadId.make("thread-1"), title: "Fix login", @@ -158,6 +161,27 @@ describe("Discord channel model selection", () => { ).toBe("Fix login ✅\n\nFixed the login flow and added coverage."); expect(taskStatusText(task)).not.toContain("Model:"); expect(taskStatusText(task)).not.toContain("Target:"); + const rendered = renderToIR(taskStatusUi(task)); + expect(rendered).toMatchObject([ + { + type: "message", + props: { + fallbackText: "Fix login 🔄", + children: [{ type: "section" }, { type: "context" }], + }, + }, + ]); + expect(JSON.stringify(rendered)).toContain("Powered by CopilotKit"); + expect( + isDeliverableTaskResponse(false, { ...task, state: "done", assistantResponse: "old" }), + ).toBe(false); + expect( + isDeliverableTaskResponse(true, { + ...task, + state: "done", + assistantResponse: "Final response", + }), + ).toBe(true); }); it("uses the final assistant message as the completed Discord response", () => { @@ -178,7 +202,7 @@ describe("Discord channel model selection", () => { ).toBe("The requested change is complete."); }); - it("sends and edits native Discord content without Channels UI components", async () => { + it("updates the Discord bot profile without handling message rendering", async () => { const requests: Array<{ readonly url: string; readonly init?: RequestInit }> = []; const fetchImpl = (async (url: string | URL | Request, init?: RequestInit) => { const requestUrl = String(url); @@ -195,27 +219,15 @@ describe("Discord channel model selection", () => { }, ); }) as typeof fetch; - const client = createDiscordTextClient("secret", fetchImpl); + const client = createDiscordProfileClient("secret", fetchImpl); - const ref = await client.post("channel-1", "Queued"); - await client.update(ref, "Running"); await client.ensureBotUsername("copilot"); await client.setGuildNickname("guild-1", "copilot"); - expect(requests.map(({ init }) => init?.method)).toEqual([ - "POST", - "PATCH", - "GET", - "PATCH", - "PATCH", - ]); - expect(requests[1]?.url).toContain("/channels/channel-1/messages/message-1"); - expect(requests[3]?.url).toContain("/users/@me"); - expect(requests[4]?.url).toContain("/guilds/guild-1/members/@me"); - expect(JSON.parse(String(requests[0]?.init?.body))).toEqual({ - content: "Queued", - }); - expect(JSON.parse(String(requests[3]?.init?.body))).toEqual({ username: "copilot" }); - expect(JSON.parse(String(requests[4]?.init?.body))).toEqual({ nick: "copilot" }); + expect(requests.map(({ init }) => init?.method)).toEqual(["GET", "PATCH", "PATCH"]); + expect(requests[1]?.url).toContain("/users/@me"); + expect(requests[2]?.url).toContain("/guilds/guild-1/members/@me"); + expect(JSON.parse(String(requests[1]?.init?.body))).toEqual({ username: "copilot" }); + expect(JSON.parse(String(requests[2]?.init?.body))).toEqual({ nick: "copilot" }); }); }); diff --git a/apps/server/src/channels/T3CodeDiscordChannel.ts b/apps/server/src/channels/T3CodeDiscordChannel.ts index 81fa369c926..a9aa7128d4c 100644 --- a/apps/server/src/channels/T3CodeDiscordChannel.ts +++ b/apps/server/src/channels/T3CodeDiscordChannel.ts @@ -1,5 +1,13 @@ import { createChannel, defineChannelCommand } from "@copilotkit/channels-core"; import { discord } from "@copilotkit/channels-discord"; +import { + Context, + Message, + Section, + type MessageRef, + type Renderable, + type Thread, +} from "@copilotkit/channels-ui"; import { CommandId, type DiscordChannelSettings, @@ -50,7 +58,10 @@ export interface T3CodeChannelOperations { config: DiscordChannelSettings, modelSelection?: ModelSelection, ) => Promise; - readonly getTaskStatus: (threadId: ThreadId) => Promise; + readonly getTaskStatus: ( + threadId: ThreadId, + assistantMessageId?: MessageId, + ) => Promise; readonly listModels: () => Promise>; readonly setDefaultModel: (modelSelection: ModelSelection) => Promise; } @@ -95,19 +106,12 @@ export function resolveDiscordModel( return models.find((model) => model.value === value); } -type ChannelThread = { +type ChannelThread = Pick & { readonly state: () => Promise; readonly setState: (value: unknown) => Promise; }; -interface DiscordTextMessageRef { - readonly channelId: string; - readonly messageId: string; -} - -interface DiscordTextClient { - readonly post: (channelId: string, text: string) => Promise; - readonly update: (ref: DiscordTextMessageRef, text: string) => Promise; +interface DiscordProfileClient { readonly ensureBotUsername: (username: string) => Promise; readonly setGuildNickname: (guildId: string, nickname: string) => Promise; } @@ -115,6 +119,8 @@ interface DiscordTextClient { interface ActiveDiscordChannel { readonly fingerprint: string; readonly refreshTask: (threadId: ThreadId) => Promise; + readonly settleTask: (threadId: ThreadId) => Promise; + readonly deliverAssistantMessage: (threadId: ThreadId, messageId: MessageId) => Promise; readonly refreshPendingTasks: () => Promise; readonly stop: () => Promise; } @@ -193,6 +199,18 @@ export function taskStatusText(task: ChannelTaskStatus): string { } } +export function taskStatusUi(task: ChannelTaskStatus): Renderable { + const text = taskStatusText(task); + return Message({ + fallbackText: text, + children: [Section({ children: text }), Context({ children: "Powered by CopilotKit" })], + }); +} + +export function isDeliverableTaskResponse(settled: boolean, task: ChannelTaskStatus): boolean { + return settled && task.state === "done" && task.assistantResponse !== null; +} + export function assistantResponseText(input: { readonly assistantMessageId: string | null | undefined; readonly turnId: string | null | undefined; @@ -215,10 +233,10 @@ export function assistantResponseText(input: { return text && text.length > 0 ? text : null; } -export function createDiscordTextClient( +export function createDiscordProfileClient( botToken: string, fetchImpl: typeof fetch = fetch, -): DiscordTextClient { +): DiscordProfileClient { const request = async (url: string, method: "GET" | "POST" | "PATCH", body?: unknown) => { const response = await fetchImpl(url, { method, @@ -235,25 +253,6 @@ export function createDiscordTextClient( }; return { - async post(channelId, text) { - const response = await request( - `https://discord.com/api/v10/channels/${channelId}/messages`, - "POST", - { content: text.slice(0, 2_000) }, - ); - const payload = (await response.json()) as { readonly id?: unknown }; - if (typeof payload.id !== "string") { - throw new Error("Discord text message response did not include a message id"); - } - return { channelId, messageId: payload.id }; - }, - async update(ref, text) { - await request( - `https://discord.com/api/v10/channels/${ref.channelId}/messages/${ref.messageId}`, - "PATCH", - { content: text.slice(0, 2_000) }, - ); - }, async ensureBotUsername(username) { const current = await request("https://discord.com/api/v10/users/@me", "GET"); const user = (await current.json()) as { readonly username?: unknown }; @@ -268,26 +267,21 @@ export function createDiscordTextClient( }; } -function discordChannelId(thread: ChannelThread): string { - const conversationKey = (thread as ChannelThread & { readonly conversationKey?: unknown }) - .conversationKey; - if (typeof conversationKey !== "string" || conversationKey.length === 0) { - throw new Error("Discord channel thread did not include a conversation key"); - } - return conversationKey; -} - function createT3CodeChannel(input: { readonly config: DiscordChannelSettings; readonly operations: T3CodeChannelOperations; readonly models: ReadonlyArray; }) { - const textClient = createDiscordTextClient(input.config.botToken); + const profileClient = createDiscordProfileClient(input.config.botToken); const linkedTasks = new Map< string, { - readonly messageRef: DiscordTextMessageRef; + readonly thread: ChannelThread; + messageRef: MessageRef; lastText: string; + settled: boolean; + settledPolls: number; + candidateAssistantMessageId: MessageId | null; terminal: boolean; } >(); @@ -303,19 +297,21 @@ function createT3CodeChannel(input: { ], }); - const postText = (thread: ChannelThread, text: string) => - textClient.post(discordChannelId(thread), text); + const postText = (thread: ChannelThread, text: string) => thread.post(text); - const updateLinkedTask = async (threadId: ThreadId, status: ChannelTaskStatus) => { + const updateLinkedTask = async ( + threadId: ThreadId, + status: ChannelTaskStatus, + terminal = false, + ) => { const linked = linkedTasks.get(threadId); if (!linked) return; const nextText = taskStatusText(status); if (nextText !== linked.lastText) { - await textClient.update(linked.messageRef, nextText); + linked.messageRef = await linked.thread.update(linked.messageRef, taskStatusUi(status)); linked.lastText = nextText; } - linked.terminal = - status.state === "failed" || (status.state === "done" && status.assistantResponse !== null); + linked.terminal = terminal || status.state === "failed"; }; const postStatus = async (thread: ChannelThread, threadId: ThreadId) => { @@ -326,10 +322,16 @@ function createT3CodeChannel(input: { } const linked = linkedTasks.get(threadId); if (linked) { - await updateLinkedTask(threadId, status); + if (status.state !== "done" || status.assistantResponse !== null) { + await updateLinkedTask( + threadId, + status, + status.state === "failed" || isDeliverableTaskResponse(linked.settled, status), + ); + } return; } - await postText(thread, taskStatusText(status)); + await thread.post(taskStatusUi(status)); }; const handleText = async (thread: ChannelThread, rawText: string) => { @@ -363,8 +365,16 @@ function createT3CodeChannel(input: { t3ThreadId: task.threadId, } satisfies LinkedConversationState); const initialText = taskStatusText(task); - const messageRef = await postText(thread, initialText); - linkedTasks.set(task.threadId, { messageRef, lastText: initialText, terminal: false }); + const messageRef = await thread.post(taskStatusUi(task)); + linkedTasks.set(task.threadId, { + thread, + messageRef, + lastText: initialText, + settled: false, + settledPolls: 0, + candidateAssistantMessageId: null, + terminal: false, + }); await postStatus(thread, task.threadId); } catch { await postText( @@ -446,23 +456,69 @@ function createT3CodeChannel(input: { if (!linked || linked.terminal) return; const task = await input.operations.getTaskStatus(threadId); if (!task) return; - await updateLinkedTask(threadId, task); + if (task.state === "failed") { + await updateLinkedTask(threadId, task, true); + return; + } + if (task.state === "queued" || task.state === "running") { + await updateLinkedTask(threadId, task); + } + }; + + const settleTask = async (threadId: ThreadId) => { + const linked = linkedTasks.get(threadId); + if (!linked || linked.terminal) return; + // ProviderRuntimeIngestion projects `ready` before it flushes the buffered + // final assistant message. Mark the lifecycle boundary, but wait for the + // subsequent non-streaming message event before completing Discord. + linked.settled = true; + linked.settledPolls = 0; + linked.candidateAssistantMessageId = null; + const task = await input.operations.getTaskStatus(threadId); + if (task?.state === "failed") { + await updateLinkedTask(threadId, task, true); + } + }; + + const deliverAssistantMessage = async (threadId: ThreadId, messageId: MessageId) => { + const linked = linkedTasks.get(threadId); + if (!linked || linked.terminal || !linked.settled) return; + // A turn can flush multiple assistant segments. Keep the newest exact + // message id and let the quiet-period poll deliver only the final segment. + linked.candidateAssistantMessageId = messageId; + linked.settledPolls = 0; }; return { channel, refreshTask, + settleTask, + deliverAssistantMessage, refreshPendingTasks: async () => { await Promise.all( Array.from(linkedTasks) .filter(([, linked]) => !linked.terminal) - .map(([threadId]) => refreshTask(ThreadId.make(threadId))), + .map(async ([threadId, linked]) => { + const id = ThreadId.make(threadId); + if (!linked.settled) { + await refreshTask(id); + return; + } + linked.settledPolls += 1; + if (linked.settledPolls < 2) return; + const task = await input.operations.getTaskStatus( + id, + linked.candidateAssistantMessageId ?? undefined, + ); + if (!task || !isDeliverableTaskResponse(linked.settled, task)) return; + await updateLinkedTask(id, task, true); + }), ); }, setDisplayName: async () => { - const updates = [textClient.ensureBotUsername("copilot")]; + const updates = [profileClient.ensureBotUsername("copilot")]; if (input.config.guildId.length > 0) { - updates.push(textClient.setGuildNickname(input.config.guildId, "copilot")); + updates.push(profileClient.setGuildNickname(input.config.guildId, "copilot")); } const results = await Promise.allSettled(updates); if (results.some((result) => result.status === "fulfilled")) return; @@ -585,12 +641,19 @@ const makeOperations = Effect.gen(function* () { const getTaskStatusEffect = Effect.fn("T3CodeDiscordChannel.getTaskStatus")(function* ( threadId: ThreadId, + preferredAssistantMessageId?: MessageId, ) { const threadOption = yield* projectionSnapshotQuery.getThreadDetailById(threadId); if (Option.isNone(threadOption)) return null; const thread = threadOption.value; const state = (() => { - if (thread.latestTurn?.state === "error" || thread.session?.status === "error") { + if ( + thread.latestTurn?.state === "error" || + thread.latestTurn?.state === "interrupted" || + thread.session?.status === "error" || + thread.session?.status === "interrupted" || + thread.session?.status === "stopped" + ) { return "failed" as const; } if (thread.latestTurn?.state === "completed") return "done" as const; @@ -601,8 +664,8 @@ const makeOperations = Effect.gen(function* () { })(); const threadEnvMode = thread.worktreePath === null ? ("local" as const) : ("worktree" as const); const assistantResponse = assistantResponseText({ - assistantMessageId: thread.latestTurn?.assistantMessageId, - turnId: thread.latestTurn?.turnId, + assistantMessageId: preferredAssistantMessageId ?? thread.latestTurn?.assistantMessageId, + turnId: preferredAssistantMessageId ? undefined : thread.latestTurn?.turnId, messages: thread.messages, }); return { @@ -619,7 +682,8 @@ const makeOperations = Effect.gen(function* () { return { startTask: (prompt, config, modelSelection) => runPromise(startTaskEffect(prompt, config, modelSelection)), - getTaskStatus: (threadId) => runPromise(getTaskStatusEffect(threadId)), + getTaskStatus: (threadId, assistantMessageId) => + runPromise(getTaskStatusEffect(threadId, assistantMessageId)), listModels: () => runPromise(providerRegistry.getProviders.pipe(Effect.map(discordModelOptions))), setDefaultModel: (modelSelection) => @@ -691,6 +755,8 @@ export const layer = Layer.effectDiscard( yield* Ref.set(activeRef, { fingerprint, refreshTask: created.refreshTask, + settleTask: created.settleTask, + deliverAssistantMessage: created.deliverAssistantMessage, refreshPendingTasks: created.refreshPendingTasks, stop: created.stop, }); @@ -708,20 +774,36 @@ export const layer = Layer.effectDiscard( ); yield* forkParked( Stream.runForEach(orchestrationEngine.streamDomainEvents, (event) => { - if ( - event.type !== "thread.turn-start-requested" && - event.type !== "thread.session-set" && - event.type !== "thread.turn-diff-completed" - ) { + if (event.type === "thread.message-sent") { + if (event.payload.role !== "assistant" || event.payload.streaming) { + return Effect.void; + } + return Ref.get(activeRef).pipe( + Effect.flatMap((active) => + active + ? Effect.tryPromise(() => + active.deliverAssistantMessage(event.payload.threadId, event.payload.messageId), + ).pipe(Effect.ignoreCause({ log: true })) + : Effect.void, + ), + ); + } + if (event.type !== "thread.session-set" && event.type !== "thread.turn-diff-completed") { return Effect.void; } return Ref.get(activeRef).pipe( Effect.flatMap((active) => - active - ? Effect.tryPromise(() => active.refreshTask(event.payload.threadId)).pipe( - Effect.ignoreCause({ log: true }), - ) - : Effect.void, + !active + ? Effect.void + : event.type === "thread.session-set" && + (event.payload.session.status === "ready" || + event.payload.session.status === "idle") + ? Effect.tryPromise(() => active.settleTask(event.payload.threadId)).pipe( + Effect.ignoreCause({ log: true }), + ) + : Effect.tryPromise(() => active.refreshTask(event.payload.threadId)).pipe( + Effect.ignoreCause({ log: true }), + ), ), ); }), From c777272677612422cc6324db98351a4e5c1319be Mon Sep 17 00:00:00 2001 From: Shivam Sharma <91240327+shivamhwp@users.noreply.github.com> Date: Fri, 7 Aug 2026 11:32:32 +0530 Subject: [PATCH 11/12] fix(channels): reply with completed T3 output --- .../src/channels/T3CodeDiscordChannel.test.ts | 32 ++++- .../src/channels/T3CodeDiscordChannel.ts | 134 +++++++++++++----- 2 files changed, 126 insertions(+), 40 deletions(-) diff --git a/apps/server/src/channels/T3CodeDiscordChannel.test.ts b/apps/server/src/channels/T3CodeDiscordChannel.test.ts index 638cb74eb54..4bad93d99fb 100644 --- a/apps/server/src/channels/T3CodeDiscordChannel.test.ts +++ b/apps/server/src/channels/T3CodeDiscordChannel.test.ts @@ -14,13 +14,15 @@ import { renderToIR } from "@copilotkit/channels-ui"; import { assistantResponseText, channelBranchName, - createDiscordProfileClient, + channelTaskState, + createDiscordClient, discordModelOptions, isDeliverableTaskResponse, isDiscordChannelConfigured, resolveDiscordModel, taskStatusText, taskStatusUi, + taskResponseUi, } from "./T3CodeDiscordChannel.ts"; const decodeDiscordChannelSettings = Schema.decodeSync(DiscordChannelSettings); @@ -158,7 +160,7 @@ describe("Discord channel model selection", () => { state: "done", assistantResponse: "Fixed the login flow and added coverage.", }), - ).toBe("Fix login ✅\n\nFixed the login flow and added coverage."); + ).toBe("Fix login ✅"); expect(taskStatusText(task)).not.toContain("Model:"); expect(taskStatusText(task)).not.toContain("Target:"); const rendered = renderToIR(taskStatusUi(task)); @@ -172,6 +174,9 @@ describe("Discord channel model selection", () => { }, ]); expect(JSON.stringify(rendered)).toContain("Powered by CopilotKit"); + expect(JSON.stringify(renderToIR(taskResponseUi("Final response")))).toContain( + "Final response", + ); expect( isDeliverableTaskResponse(false, { ...task, state: "done", assistantResponse: "old" }), ).toBe(false); @@ -182,6 +187,13 @@ describe("Discord channel model selection", () => { assistantResponse: "Final response", }), ).toBe(true); + expect( + channelTaskState({ + latestTurnState: null, + sessionStatus: "ready", + assistantResponse: "Final response", + }), + ).toBe("done"); }); it("uses the final assistant message as the completed Discord response", () => { @@ -202,7 +214,7 @@ describe("Discord channel model selection", () => { ).toBe("The requested change is complete."); }); - it("updates the Discord bot profile without handling message rendering", async () => { + it("updates the bot profile and sends a rendered reply to the status message", async () => { const requests: Array<{ readonly url: string; readonly init?: RequestInit }> = []; const fetchImpl = (async (url: string | URL | Request, init?: RequestInit) => { const requestUrl = String(url); @@ -219,15 +231,25 @@ describe("Discord channel model selection", () => { }, ); }) as typeof fetch; - const client = createDiscordProfileClient("secret", fetchImpl); + const client = createDiscordClient("secret", fetchImpl); await client.ensureBotUsername("copilot"); await client.setGuildNickname("guild-1", "copilot"); + await client.reply( + { id: "status-1", channelId: "channel-1" }, + taskResponseUi("The task is complete."), + ); - expect(requests.map(({ init }) => init?.method)).toEqual(["GET", "PATCH", "PATCH"]); + expect(requests.map(({ init }) => init?.method)).toEqual(["GET", "PATCH", "PATCH", "POST"]); expect(requests[1]?.url).toContain("/users/@me"); expect(requests[2]?.url).toContain("/guilds/guild-1/members/@me"); expect(JSON.parse(String(requests[1]?.init?.body))).toEqual({ username: "copilot" }); expect(JSON.parse(String(requests[2]?.init?.body))).toEqual({ nick: "copilot" }); + const replyBody = JSON.parse(String(requests[3]?.init?.body)); + expect(replyBody.message_reference).toEqual({ + message_id: "status-1", + channel_id: "channel-1", + }); + expect(JSON.stringify(replyBody.components)).toContain("The task is complete."); }); }); diff --git a/apps/server/src/channels/T3CodeDiscordChannel.ts b/apps/server/src/channels/T3CodeDiscordChannel.ts index a9aa7128d4c..ffa02377bce 100644 --- a/apps/server/src/channels/T3CodeDiscordChannel.ts +++ b/apps/server/src/channels/T3CodeDiscordChannel.ts @@ -1,5 +1,5 @@ import { createChannel, defineChannelCommand } from "@copilotkit/channels-core"; -import { discord } from "@copilotkit/channels-discord"; +import { discord, renderDiscordMessage } from "@copilotkit/channels-discord"; import { Context, Message, @@ -7,6 +7,7 @@ import { type MessageRef, type Renderable, type Thread, + renderToIR, } from "@copilotkit/channels-ui"; import { CommandId, @@ -111,9 +112,10 @@ type ChannelThread = Pick & { readonly setState: (value: unknown) => Promise; }; -interface DiscordProfileClient { +interface DiscordClient { readonly ensureBotUsername: (username: string) => Promise; readonly setGuildNickname: (guildId: string, nickname: string) => Promise; + readonly reply: (messageRef: MessageRef, ui: Renderable) => Promise; } interface ActiveDiscordChannel { @@ -193,7 +195,7 @@ export function taskStatusText(task: ChannelTaskStatus): string { case "running": return `${task.title} 🔄`; case "done": - return `${task.title} ✅\n\n${task.assistantResponse ?? "Done."}`; + return `${task.title} ✅`; case "failed": return `${task.title} ❌\n\nTask failed.`; } @@ -207,10 +209,51 @@ export function taskStatusUi(task: ChannelTaskStatus): Renderable { }); } +export function taskResponseUi(response: string): Renderable { + return Message({ + fallbackText: response, + children: [Section({ children: response }), Context({ children: "Powered by CopilotKit" })], + }); +} + export function isDeliverableTaskResponse(settled: boolean, task: ChannelTaskStatus): boolean { return settled && task.state === "done" && task.assistantResponse !== null; } +export function channelTaskState(input: { + readonly latestTurnState: "running" | "interrupted" | "completed" | "error" | null | undefined; + readonly sessionStatus: + | "idle" + | "starting" + | "running" + | "ready" + | "interrupted" + | "stopped" + | "error" + | null + | undefined; + readonly assistantResponse: string | null; +}): ChannelTaskStatus["state"] { + if ( + input.latestTurnState === "error" || + input.latestTurnState === "interrupted" || + input.sessionStatus === "error" || + input.sessionStatus === "interrupted" || + input.sessionStatus === "stopped" + ) { + return "failed"; + } + if ( + input.latestTurnState === "completed" || + ((input.sessionStatus === "ready" || input.sessionStatus === "idle") && + input.assistantResponse !== null) + ) { + return "done"; + } + if (input.latestTurnState === "running" || input.sessionStatus === "running") return "running"; + return "queued"; +} + export function assistantResponseText(input: { readonly assistantMessageId: string | null | undefined; readonly turnId: string | null | undefined; @@ -233,10 +276,10 @@ export function assistantResponseText(input: { return text && text.length > 0 ? text : null; } -export function createDiscordProfileClient( +export function createDiscordClient( botToken: string, fetchImpl: typeof fetch = fetch, -): DiscordProfileClient { +): DiscordClient { const request = async (url: string, method: "GET" | "POST" | "PATCH", body?: unknown) => { const response = await fetchImpl(url, { method, @@ -264,6 +307,22 @@ export function createDiscordProfileClient( nick: nickname, }); }, + async reply(messageRef, ui) { + const channelId = messageRef.channelId; + if (typeof channelId !== "string" || channelId.length === 0) { + throw new Error("Discord message reference did not include a channel id"); + } + const rendered = renderDiscordMessage(renderToIR(ui)); + await request(`https://discord.com/api/v10/channels/${channelId}/messages`, "POST", { + components: rendered.components.map((component) => component.toJSON()), + flags: rendered.flags, + message_reference: { + message_id: messageRef.id, + channel_id: channelId, + }, + allowed_mentions: { replied_user: false }, + }); + }, }; } @@ -272,7 +331,7 @@ function createT3CodeChannel(input: { readonly operations: T3CodeChannelOperations; readonly models: ReadonlyArray; }) { - const profileClient = createDiscordProfileClient(input.config.botToken); + const discordClient = createDiscordClient(input.config.botToken); const linkedTasks = new Map< string, { @@ -314,6 +373,14 @@ function createT3CodeChannel(input: { linked.terminal = terminal || status.state === "failed"; }; + const completeLinkedTask = async (threadId: ThreadId, status: ChannelTaskStatus) => { + const linked = linkedTasks.get(threadId); + if (!linked || linked.terminal || status.assistantResponse === null) return; + await updateLinkedTask(threadId, status); + await discordClient.reply(linked.messageRef, taskResponseUi(status.assistantResponse)); + linked.terminal = true; + }; + const postStatus = async (thread: ChannelThread, threadId: ThreadId) => { const status = await input.operations.getTaskStatus(threadId); if (!status) { @@ -322,16 +389,17 @@ function createT3CodeChannel(input: { } const linked = linkedTasks.get(threadId); if (linked) { - if (status.state !== "done" || status.assistantResponse !== null) { - await updateLinkedTask( - threadId, - status, - status.state === "failed" || isDeliverableTaskResponse(linked.settled, status), - ); + if (isDeliverableTaskResponse(linked.settled, status)) { + await completeLinkedTask(threadId, status); + } else if (status.state !== "done") { + await updateLinkedTask(threadId, status, status.state === "failed"); } return; } - await thread.post(taskStatusUi(status)); + const messageRef = await thread.post(taskStatusUi(status)); + if (status.state === "done" && status.assistantResponse !== null) { + await discordClient.reply(messageRef, taskResponseUi(status.assistantResponse)); + } }; const handleText = async (thread: ChannelThread, rawText: string) => { @@ -511,14 +579,14 @@ function createT3CodeChannel(input: { linked.candidateAssistantMessageId ?? undefined, ); if (!task || !isDeliverableTaskResponse(linked.settled, task)) return; - await updateLinkedTask(id, task, true); + await completeLinkedTask(id, task); }), ); }, setDisplayName: async () => { - const updates = [profileClient.ensureBotUsername("copilot")]; + const updates = [discordClient.ensureBotUsername("copilot")]; if (input.config.guildId.length > 0) { - updates.push(profileClient.setGuildNickname(input.config.guildId, "copilot")); + updates.push(discordClient.setGuildNickname(input.config.guildId, "copilot")); } const results = await Promise.allSettled(updates); if (results.some((result) => result.status === "fulfilled")) return; @@ -646,28 +714,24 @@ const makeOperations = Effect.gen(function* () { const threadOption = yield* projectionSnapshotQuery.getThreadDetailById(threadId); if (Option.isNone(threadOption)) return null; const thread = threadOption.value; - const state = (() => { - if ( - thread.latestTurn?.state === "error" || - thread.latestTurn?.state === "interrupted" || - thread.session?.status === "error" || - thread.session?.status === "interrupted" || - thread.session?.status === "stopped" - ) { - return "failed" as const; - } - if (thread.latestTurn?.state === "completed") return "done" as const; - if (thread.latestTurn?.state === "running" || thread.session?.status === "running") { - return "running" as const; - } - return "queued" as const; - })(); - const threadEnvMode = thread.worktreePath === null ? ("local" as const) : ("worktree" as const); + const latestCompletedAssistantMessage = thread.messages.findLast( + (message) => message.role === "assistant" && !message.streaming, + ); + const resolvedAssistantMessageId = + preferredAssistantMessageId ?? + thread.latestTurn?.assistantMessageId ?? + latestCompletedAssistantMessage?.id; const assistantResponse = assistantResponseText({ - assistantMessageId: preferredAssistantMessageId ?? thread.latestTurn?.assistantMessageId, - turnId: preferredAssistantMessageId ? undefined : thread.latestTurn?.turnId, + assistantMessageId: resolvedAssistantMessageId, + turnId: resolvedAssistantMessageId ? undefined : thread.latestTurn?.turnId, messages: thread.messages, }); + const state = channelTaskState({ + latestTurnState: thread.latestTurn?.state, + sessionStatus: thread.session?.status, + assistantResponse, + }); + const threadEnvMode = thread.worktreePath === null ? ("local" as const) : ("worktree" as const); return { threadId, title: thread.title, From 1455d23a87a25bebf71e4f7ccf5174674a3940e3 Mon Sep 17 00:00:00 2001 From: Shivam Sharma <91240327+shivamhwp@users.noreply.github.com> Date: Fri, 7 Aug 2026 12:53:31 +0530 Subject: [PATCH 12/12] fix(discord): use copilotkit bot name --- apps/server/src/channels/T3CodeDiscordChannel.test.ts | 8 ++++---- apps/server/src/channels/T3CodeDiscordChannel.ts | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/apps/server/src/channels/T3CodeDiscordChannel.test.ts b/apps/server/src/channels/T3CodeDiscordChannel.test.ts index 4bad93d99fb..991efabd83f 100644 --- a/apps/server/src/channels/T3CodeDiscordChannel.test.ts +++ b/apps/server/src/channels/T3CodeDiscordChannel.test.ts @@ -233,8 +233,8 @@ describe("Discord channel model selection", () => { }) as typeof fetch; const client = createDiscordClient("secret", fetchImpl); - await client.ensureBotUsername("copilot"); - await client.setGuildNickname("guild-1", "copilot"); + await client.ensureBotUsername("copilotkit"); + await client.setGuildNickname("guild-1", "copilotkit"); await client.reply( { id: "status-1", channelId: "channel-1" }, taskResponseUi("The task is complete."), @@ -243,8 +243,8 @@ describe("Discord channel model selection", () => { expect(requests.map(({ init }) => init?.method)).toEqual(["GET", "PATCH", "PATCH", "POST"]); expect(requests[1]?.url).toContain("/users/@me"); expect(requests[2]?.url).toContain("/guilds/guild-1/members/@me"); - expect(JSON.parse(String(requests[1]?.init?.body))).toEqual({ username: "copilot" }); - expect(JSON.parse(String(requests[2]?.init?.body))).toEqual({ nick: "copilot" }); + expect(JSON.parse(String(requests[1]?.init?.body))).toEqual({ username: "copilotkit" }); + expect(JSON.parse(String(requests[2]?.init?.body))).toEqual({ nick: "copilotkit" }); const replyBody = JSON.parse(String(requests[3]?.init?.body)); expect(replyBody.message_reference).toEqual({ message_id: "status-1", diff --git a/apps/server/src/channels/T3CodeDiscordChannel.ts b/apps/server/src/channels/T3CodeDiscordChannel.ts index ffa02377bce..a2a9d7471cd 100644 --- a/apps/server/src/channels/T3CodeDiscordChannel.ts +++ b/apps/server/src/channels/T3CodeDiscordChannel.ts @@ -584,9 +584,9 @@ function createT3CodeChannel(input: { ); }, setDisplayName: async () => { - const updates = [discordClient.ensureBotUsername("copilot")]; + const updates = [discordClient.ensureBotUsername("copilotkit")]; if (input.config.guildId.length > 0) { - updates.push(discordClient.setGuildNickname(input.config.guildId, "copilot")); + updates.push(discordClient.setGuildNickname(input.config.guildId, "copilotkit")); } const results = await Promise.allSettled(updates); if (results.some((result) => result.status === "fulfilled")) return;