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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion apps/agent-worker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,10 @@ the existing account state and R2 lifecycle phases.

Run creation validates the gateway payload with the shared `CreateRunSchema` from
`packages/types` before selecting the run-scoped `AgentRun` Durable Object. The
database binds a gateway-hashed idempotency key to the exact body and thread. After the
validated request keeps the user's exact message separate from explicit non-app run intent,
selected skill, and selected connected-app metadata. Those selections remain in the
checkpointed Workflow input and request context; they are never encoded into visible prompt text.
The database binds a gateway-hashed idempotency key to the exact body and thread. After the
pending run and thread pointer commit, start delivery is retried and then reconciled through
an ordered run-key presence probe. A present object reconnects its stream (and finalizes a
durable Workflow admission first); only an authoritative empty response fails the nonterminal database run
Expand Down
2 changes: 2 additions & 0 deletions apps/agent-worker/src/agent-routing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,8 @@ export async function startAgentRun(
model: run.modelId,
isModelExplicit,
...(body.intent ? { runIntent: body.intent } : {}),
...(body.selectedSkill ? { selectedSkill: body.selectedSkill } : {}),
...(body.selectedTool ? { selectedTool: body.selectedTool } : {}),
...(run.projectId ? { projectId: run.projectId } : {}),
...(run.workspaceSlug ? { workspaceSlug: run.workspaceSlug } : {}),
...(run.projectMode ? { projectMode: run.projectMode } : {}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ export function createAgentRequestContext(
openrouterApiKey: credential.transportProvider === "openrouter" ? credential.apiKey : undefined,
projectMode: input.projectMode,
runIntent: input.runIntent,
selectedSkill: input.selectedSkill,
selectedTool: input.selectedTool,
runId: input.runId,
taskMessage: input.messageText,
...(isSkillCreator ? { userSkillCreator } : {}),
Expand Down
10 changes: 8 additions & 2 deletions apps/agent-worker/src/durable-objects/agent-run-schemas.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { CatalogModelIdSchema, LogicalModelIdSchema } from "@cheatcode/types";
import { ProjectModeSchema, RunIntentSchema } from "@cheatcode/types/api";
import {
CatalogModelIdSchema,
IntegrationNameSchema,
LogicalModelIdSchema,
} from "@cheatcode/types";
import { ProjectModeSchema, RunIntentSchema, SelectedSkillSchema } from "@cheatcode/types/api";
import { z } from "zod";

export const StartRunInputSchema = z
Expand All @@ -16,6 +20,8 @@ export const StartRunInputSchema = z
// automatic provider fallback so a pinned model is never silently replaced.
isModelExplicit: z.boolean(),
runIntent: RunIntentSchema.optional(),
selectedSkill: SelectedSkillSchema.optional(),
selectedTool: IntegrationNameSchema.optional(),
projectMode: ProjectModeSchema.default("general"),
isFirstRun: z.boolean().default(false),
agentDisplayName: z.string().trim().min(1).max(80).optional(),
Expand Down
13 changes: 9 additions & 4 deletions apps/web/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,10 @@ The composer keeps three independent product concepts separate:

- `ComposerWorkIntentId` describes what the user wants to accomplish. Web app,
mobile app, slides, research, data, documents, and media are discoverable
composer choices. These choices guide prompt context; they are not all project
modes. The generic Slides choice activates the general PPTX workflow, never the
composer choices. Non-app choices cross the run API as validated intent metadata,
so an explicit Documents choice cannot be reclassified as Web merely because the
requested document discusses a website. These choices are not all project modes.
The generic Slides choice activates the general PPTX workflow, never the
fundraising-specific pitch-deck skill. Explicit PPTX and pitch-deck skill deep
links both select the Slides intent while preserving the skill the user chose.
- `AppBuildTarget` is only the runtime topology for generated applications:
Expand All @@ -100,9 +102,12 @@ The composer keeps three independent product concepts separate:
focus from Files. Explicit browser takeover still selects Browser. The selected tab is not persisted
across unrelated chats.

Composer selections are request metadata: `@`-selected skills and connected apps are
validated separately from the exact user-authored text and cross the opaque chat handoff.
The client never rewrites a visible message with a slash command or internal toolkit preamble.
Signed-out launch handoff validates and restores `buildTarget`, model, and
public GitHub repository state before chat creation. The opaque prompt and
constrained run intent use the same one-shot handoff path. No `surface` query
public GitHub repository state before chat creation. The opaque prompt,
constrained run intent, skill, and connected-app selection use the same one-shot handoff path. No `surface` query
parameter or persisted `app` tab alias is supported.

## Env
Expand Down
15 changes: 12 additions & 3 deletions apps/web/src/components/chat/chat-panel-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { ChatOnDataCallback, ChatStatus } from "ai";
import { useRouter } from "next/navigation";
import { useCallback, useDeferredValue, useEffect, useMemo, useRef, useState } from "react";
import { toast } from "sonner";
import type { ChatSubmissionSelection } from "@/components/chat/chat-panel-submission";
import {
mergeLoadedMessageHistory,
type PendingSubmission,
Expand Down Expand Up @@ -44,6 +45,8 @@ export interface ChatPanelProps {
hasOlderMessages: boolean;
initialMessages?: CheatcodeUIMessage[] | undefined;
initialRunIntent?: import("@cheatcode/types/api").RunIntent | null | undefined;
initialSelectedSkill?: null | string | undefined;
initialSelectedTool?: import("@cheatcode/types").IntegrationName | null | undefined;
isLoadingOlderMessages: boolean;
latestModelId: null | string;
onLoadOlderMessages: () => Promise<CheatcodeUIMessage[]>;
Expand Down Expand Up @@ -161,7 +164,6 @@ function usePanelSubmission(
getToken: runtime.getToken,
hasReceivedStreamDataRef: runtime.hasReceivedStreamDataRef,
hasSubmittedRef: runtime.hasSubmittedRef,
initialRunIntent: input.initialRunIntent ?? null,
onSubmitDraft: input.onSubmitDraft,
pendingSubmissionRef: runtime.pendingSubmissionRef,
project: input.project,
Expand All @@ -178,7 +180,6 @@ function usePanelSubmission(
[
input.activeRunId,
input.onSubmitDraft,
input.initialRunIntent,
input.project,
input.threadId,
input.threadTitle,
Expand Down Expand Up @@ -213,11 +214,19 @@ function useAutoSubmitPrompt(
if (runtime.store.draft.trim() !== prompt) {
runtime.store.setDraft(input.threadId, prompt);
}
if (submitText(prompt, input.project)) {
const selection: ChatSubmissionSelection = {
intent: input.initialRunIntent ?? null,
selectedSkill: input.initialSelectedSkill ?? null,
selectedTool: input.initialSelectedTool ?? null,
};
if (submitText(prompt, input.project, selection)) {
autoSubmittedPromptRef.current = prompt;
}
}, [
input.autoSubmitPrompt,
input.initialRunIntent,
input.initialSelectedSkill,
input.initialSelectedTool,
input.project,
input.threadId,
runtime.store.draft,
Expand Down
18 changes: 15 additions & 3 deletions apps/web/src/components/chat/chat-panel-submission.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { CheatcodeUIMessage } from "@cheatcode/types";
import type { ProjectSummary, ThreadMessage } from "@cheatcode/types/api";
import type { CheatcodeUIMessage, IntegrationName } from "@cheatcode/types";
import type { ProjectSummary, RunIntent, ThreadMessage } from "@cheatcode/types/api";
import type { QueryClient } from "@tanstack/react-query";
import { toast } from "sonner";
import { buildExistingProjectParams, launchIntoProject } from "@/lib/api/home-launch";
Expand All @@ -12,6 +12,12 @@ import {
} from "@/lib/api/project-thread";
import { sidebarKeys, threadKeys } from "@/lib/api/query-keys";

export interface ChatSubmissionSelection {
intent: RunIntent | null;
selectedSkill: string | null;
selectedTool: IntegrationName | null;
}

export interface PendingSubmission {
messageId: string;
restoreToComposer: boolean;
Expand Down Expand Up @@ -70,6 +76,7 @@ export async function routePromptToProjectTarget(input: {
queryClient: QueryClient;
router: PromptRouter;
selectedModel: null | string;
selection: Partial<ChatSubmissionSelection>;
setDraft: (threadId: string, value: string) => void;
targetProject: ProjectSummary | null;
threadId: string;
Expand Down Expand Up @@ -144,7 +151,12 @@ function completeProjectTargetNavigation(
input: Parameters<typeof routePromptToProjectTarget>[0],
targetThreadId: string,
): void {
const handoff = buildExistingProjectParams(input.prompt).toString();
const handoff = buildExistingProjectParams({
prompt: input.prompt,
...(input.selection.intent ? { intent: input.selection.intent } : {}),
...(input.selection.selectedSkill ? { selectedSkill: input.selection.selectedSkill } : {}),
...(input.selection.selectedTool ? { selectedTool: input.selection.selectedTool } : {}),
}).toString();
input.setDraft(input.threadId, "");
void input.queryClient.invalidateQueries({ queryKey: sidebarKeys.chats });
void input.queryClient.invalidateQueries({ queryKey: sidebarKeys.projectThreads });
Expand Down
2 changes: 2 additions & 0 deletions apps/web/src/components/chat/chat-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export function createChatTransport(
intent: body?.["intent"],
message: runRequestMessage(latestMessage),
model: body?.["model"],
selectedSkill: body?.["selectedSkill"],
selectedTool: body?.["selectedTool"],
},
headers: {
...headers,
Expand Down
19 changes: 9 additions & 10 deletions apps/web/src/components/chat/prompt-composer-controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ import {
useState,
} from "react";
import type { RunStatus } from "@/components/chat/status-pill";
import { composePromptWithComposerContext } from "@/components/composer/composer-context-chips";
import type { ComposerMenuItem } from "@/components/composer/composer-popover";
import {
type ComposerMenuController,
Expand All @@ -30,7 +29,11 @@ type ComposerControlMenu = "model";
export interface PromptComposerProps {
onChange: (value: string) => void;
onStop: () => void;
onSubmit: (value: string, project: ProjectSummary | null) => boolean;
onSubmit: (
value: string,
project: ProjectSummary | null,
selection: { selectedSkill: string | null; selectedTool: IntegrationName | null },
) => boolean;
project: ProjectSummary | null;
resolvedModelId: null | string;
status: RunStatus;
Expand Down Expand Up @@ -231,14 +234,10 @@ function createComposerSubmission({
value,
}: ComposerSubmissionOptions) {
function submitComposerValue() {
const wasAccepted = onSubmit(
composePromptWithComposerContext({
prompt: value,
skill: selection.selectedSkill,
tool: selection.selectedTool,
}),
project,
);
const wasAccepted = onSubmit(value.trim(), project, {
selectedSkill: selection.selectedSkill,
selectedTool: selection.selectedTool,
});
if (wasAccepted) {
selection.setSelectedSkill(null);
selection.setSelectedTool(null);
Expand Down
52 changes: 36 additions & 16 deletions apps/web/src/components/chat/use-chat-submission.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import type { CheatcodeUIMessage } from "@cheatcode/types";
import type { CheatcodeUIMessage, IntegrationName } from "@cheatcode/types";
import type { ProjectSummary, RunIntent } from "@cheatcode/types/api";
import type { QueryClient } from "@tanstack/react-query";
import type { ChatStatus } from "ai";
import { useCallback } from "react";
import { toast } from "sonner";
import {
type ChatSubmissionSelection,
type PendingSubmission,
type PromptRouter,
routePromptToProjectTarget,
Expand All @@ -23,7 +24,6 @@ interface ChatSubmissionInput {
getToken: () => Promise<null | string>;
hasReceivedStreamDataRef: { current: boolean };
hasSubmittedRef: { current: boolean };
initialRunIntent: RunIntent | null;
onSubmitDraft?: (() => void) | undefined;
pendingSubmissionRef: { current: PendingSubmission | null };
project: ProjectSummary | null;
Expand All @@ -40,10 +40,18 @@ interface ChatSubmissionInput {

export function useChatSubmission(input: ChatSubmissionInput): {
continueRun: () => void;
submitText: (text: string, targetProject: ProjectSummary | null) => boolean;
submitText: (
text: string,
targetProject: ProjectSummary | null,
selection?: Partial<ChatSubmissionSelection>,
) => boolean;
} {
const submitText = useCallback(
(text: string, targetProject: ProjectSummary | null): boolean => {
(
text: string,
targetProject: ProjectSummary | null,
selection: Partial<ChatSubmissionSelection> = {},
): boolean => {
if (!isValidUserMessage(text)) {
return false;
}
Expand All @@ -52,10 +60,10 @@ export function useChatSubmission(input: ChatSubmissionInput): {
}
input.hasSubmittedRef.current = true;
if (!isCurrentThreadTarget(input.project, targetProject)) {
routeSubmissionToProject(input, text, targetProject);
routeSubmissionToProject(input, text, targetProject, selection);
return true;
}
submitInCurrentThread(input, text);
submitInCurrentThread(input, text, selection);
return true;
},
[input],
Expand All @@ -73,16 +81,17 @@ export function useChatSubmission(input: ChatSubmissionInput): {
const pending = pendingSubmission(messageId, text, false);
input.pendingSubmissionRef.current = pending;
input.setRunStartedAt(pending.submittedAt);
void input.sendMessage(
userMessage(messageId, text, null),
modelBody(input.selectedModel, null),
);
void input.sendMessage(userMessage(messageId, text, null), modelBody(input.selectedModel, {}));
}, [input]);

return { continueRun, submitText };
}

function submitInCurrentThread(input: ChatSubmissionInput, text: string): void {
function submitInCurrentThread(
input: ChatSubmissionInput,
text: string,
selection: Partial<ChatSubmissionSelection>,
): void {
if (!input.threadTitle?.trim() || input.threadTitle.trim() === "New chat") {
void titleChatFromFirstPrompt(input.getToken, input.queryClient, input.threadId, text);
}
Expand All @@ -92,8 +101,8 @@ function submitInCurrentThread(input: ChatSubmissionInput, text: string): void {
input.pendingSubmissionRef.current = pending;
input.setRunStartedAt(pending.submittedAt);
void input.sendMessage(
userMessage(messageId, text, input.initialRunIntent),
modelBody(input.selectedModel, input.initialRunIntent),
userMessage(messageId, text, selection.intent ?? null),
modelBody(input.selectedModel, selection),
);
input.setDraft(input.threadId, "");
input.onSubmitDraft?.();
Expand All @@ -103,13 +112,15 @@ function routeSubmissionToProject(
input: ChatSubmissionInput,
prompt: string,
targetProject: ProjectSummary | null,
selection: Partial<ChatSubmissionSelection>,
): void {
void routePromptToProjectTarget({
getToken: input.getToken,
prompt,
queryClient: input.queryClient,
router: input.router,
selectedModel: input.selectedModel,
selection,
setDraft: input.setDraft,
targetProject,
threadId: input.threadId,
Expand Down Expand Up @@ -176,12 +187,21 @@ function userMessage(

function modelBody(
selectedModel: null | string,
intent: RunIntent | null,
): { body: { intent?: RunIntent; model?: string } } {
selection: Partial<ChatSubmissionSelection>,
): {
body: {
intent?: RunIntent;
model?: string;
selectedSkill?: string;
selectedTool?: IntegrationName;
};
} {
return {
body: {
...(intent ? { intent } : {}),
...(selection.intent ? { intent: selection.intent } : {}),
...(selectedModel ? { model: selectedModel } : {}),
...(selection.selectedSkill ? { selectedSkill: selection.selectedSkill } : {}),
...(selection.selectedTool ? { selectedTool: selection.selectedTool } : {}),
},
};
}
20 changes: 0 additions & 20 deletions apps/web/src/components/composer/composer-context-chips.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,26 +25,6 @@ function toolLabel(slug: string): string {
);
}

export function composePromptWithComposerContext({
prompt,
skill,
tool,
}: {
prompt: string;
skill: string | null;
tool: IntegrationName | null;
}): string {
const trimmed = prompt.trim();
let nextPrompt = skill && !trimmed.startsWith("/") ? `/${skill} ${trimmed}` : trimmed;
if (tool) {
nextPrompt = [
`Selected tool: ${toolLabel(tool)} (${tool}). Use the Composio integration for this request when an external app action is needed.`,
nextPrompt,
].join("\n\n");
}
return nextPrompt.trim();
}

export function ComposerContextChips({
className,
onClearSkill,
Expand Down
Loading