Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
568 lines
16 KiB
TypeScript
568 lines
16 KiB
TypeScript
import type { AgentSessionCommand } from "../../../shared/agentSessionTypes";
|
|
import type {
|
|
AgentActivityComposerOptions,
|
|
AgentActivitySubmitSettingsPatch,
|
|
AgentActivitySlashCommandPolicy
|
|
} from "@tutti-os/agent-activity-core";
|
|
import {
|
|
agentCapabilityUseDisplayPrompt,
|
|
buildAgentCapabilityUseSubmitPrompt,
|
|
parseAgentCapabilityUseInvocation
|
|
} from "./agentCapabilityUseSubmit";
|
|
import type { AgentCapabilityUse } from "./agentCapabilityUseSubmit";
|
|
import {
|
|
draftForSlashCommand,
|
|
mergeSlashCommands,
|
|
parseSlashCommandInvocation,
|
|
promptForSlashCommand
|
|
} from "./agentSlashCommands";
|
|
|
|
export type AgentSlashCommandProvider = string;
|
|
|
|
export interface AgentSlashCommandCapability {
|
|
aliases?: readonly string[];
|
|
capability: AgentCapabilityUse | "tutti";
|
|
kind: "capability";
|
|
name: string;
|
|
}
|
|
|
|
export type AgentSlashCommand =
|
|
| AgentSessionCommand
|
|
| AgentSlashCommandCapability;
|
|
|
|
export type AgentSlashCommandPolicy = AgentActivitySlashCommandPolicy;
|
|
|
|
export type SlashCommandSelectionEffect =
|
|
| {
|
|
kind: "fillDraft";
|
|
draft: string;
|
|
}
|
|
| {
|
|
kind: "activateGoalMode";
|
|
}
|
|
| {
|
|
kind: "submitPrompt";
|
|
prompt: string;
|
|
displayPrompt?: string;
|
|
requiredSettingsPatch?: AgentActivitySubmitSettingsPatch;
|
|
}
|
|
| {
|
|
kind: "showStatus";
|
|
}
|
|
| {
|
|
kind: "enablePlanMode";
|
|
}
|
|
| {
|
|
kind: "activateTuttiMode";
|
|
}
|
|
| {
|
|
kind: "enableBrowserUse";
|
|
draft: string;
|
|
}
|
|
| {
|
|
kind: "enableComputerUse";
|
|
draft: string;
|
|
}
|
|
| {
|
|
kind: "showReviewPicker";
|
|
}
|
|
| {
|
|
kind: "toggleSpeed";
|
|
};
|
|
|
|
interface ResolveSlashCommandSelectionEffectInput {
|
|
provider: AgentSlashCommandProvider;
|
|
policy?: AgentSlashCommandPolicy | null;
|
|
command: AgentSlashCommand;
|
|
currentDraft: string;
|
|
}
|
|
|
|
interface ResolveSlashCommandSubmitEffectInput {
|
|
browserSupported?: boolean;
|
|
computerSupported?: boolean;
|
|
tuttiSupported?: boolean;
|
|
provider: AgentSlashCommandProvider;
|
|
policy?: AgentSlashCommandPolicy | null;
|
|
commands: readonly AgentSlashCommand[];
|
|
draft: string;
|
|
}
|
|
|
|
interface ProviderSlashPolicy {
|
|
/** Commands submitted immediately on selection instead of filling a draft. */
|
|
immediateCommands: ReadonlySet<string>;
|
|
/** Commands that open the shared review target picker when invoked bare. */
|
|
reviewPickerCommands: ReadonlySet<string>;
|
|
/** Commands surfaced when the agent advertises none of its own. */
|
|
fallbackCommands: readonly AgentSessionCommand[];
|
|
localGoalCommands: ReadonlySet<string>;
|
|
localPlanCommands: ReadonlySet<string>;
|
|
localStatusCommands: ReadonlySet<string>;
|
|
localSpeedCommands: ReadonlySet<string>;
|
|
}
|
|
|
|
// `/plan` enables plan mode locally (a negotiated capability) rather than
|
|
// reaching the agent as a prompt; removing its badge disables it again.
|
|
// `/fast` toggles the descriptor-advertised speed dimension locally.
|
|
const BROWSER_USE_CAPABILITY_COMMAND: AgentSlashCommandCapability = {
|
|
kind: "capability",
|
|
capability: "browserUse",
|
|
name: "browser",
|
|
aliases: ["浏览器"]
|
|
};
|
|
const COMPUTER_USE_CAPABILITY_COMMAND: AgentSlashCommandCapability = {
|
|
kind: "capability",
|
|
capability: "computerUse",
|
|
name: "computer",
|
|
aliases: ["电脑"]
|
|
};
|
|
const TUTTI_CAPABILITY_COMMAND: AgentSlashCommandCapability = {
|
|
kind: "capability",
|
|
capability: "tutti",
|
|
name: "tutti"
|
|
};
|
|
const PLAN_MODE_COMMAND: AgentSessionCommand = { name: "plan" };
|
|
|
|
function providerSlashPolicy(
|
|
policy?: AgentSlashCommandPolicy | null
|
|
): ProviderSlashPolicy | undefined {
|
|
if (policy) {
|
|
const commandEffects = Array.isArray(policy.commandEffects)
|
|
? policy.commandEffects
|
|
: [];
|
|
const fallbackCommands = Array.isArray(policy.fallbackCommands)
|
|
? policy.fallbackCommands
|
|
: [];
|
|
const commandsForEffect = (
|
|
effect: AgentSlashCommandPolicy["commandEffects"][number]["effect"]
|
|
) =>
|
|
normalizedCommandSet(
|
|
commandEffects.flatMap((entry) =>
|
|
entry.effect === effect ? [entry.command] : []
|
|
)
|
|
);
|
|
return {
|
|
fallbackCommands: fallbackCommands.map((name) => ({ name })),
|
|
immediateCommands: commandsForEffect("submitImmediate"),
|
|
reviewPickerCommands: commandsForEffect("showReviewPicker"),
|
|
localGoalCommands: commandsForEffect("activateGoalMode"),
|
|
localPlanCommands: commandsForEffect("togglePlanMode"),
|
|
localStatusCommands: commandsForEffect("showStatus"),
|
|
localSpeedCommands: commandsForEffect("toggleSpeed")
|
|
};
|
|
}
|
|
return undefined;
|
|
}
|
|
|
|
export function resolveSlashCommandsForProvider({
|
|
provider: _provider,
|
|
policy,
|
|
commands,
|
|
hasCompactableContext = true,
|
|
compactSupported,
|
|
planSupported = false,
|
|
browserSupported = false,
|
|
computerSupported = false,
|
|
tuttiSupported = false
|
|
}: {
|
|
provider: AgentSlashCommandProvider;
|
|
policy?: AgentSlashCommandPolicy | null;
|
|
commands: readonly AgentSessionCommand[];
|
|
hasCompactableContext?: boolean;
|
|
/**
|
|
* Negotiated `compact` capability. `false` drops the command entirely
|
|
* (including provider fallbacks); `undefined`/`null` means unknown and
|
|
* keeps the legacy `hasCompactableContext` behavior.
|
|
*/
|
|
compactSupported?: boolean | null;
|
|
planSupported?: boolean;
|
|
browserSupported?: boolean;
|
|
computerSupported?: boolean;
|
|
tuttiSupported?: boolean;
|
|
}): AgentSlashCommand[] {
|
|
// Provider-advertised commands are runtime capabilities. Open extension
|
|
// providers do not have a built-in Tutti policy descriptor, but their ACP
|
|
// command catalog is still authoritative and must remain visible.
|
|
const descriptorCommands = commands;
|
|
const mergedEntries = mergeSlashCommands(
|
|
filterUnavailableSlashCommands(descriptorCommands, {
|
|
compactSupported,
|
|
hasCompactableContext
|
|
}),
|
|
filterUnavailableSlashCommands(fallbackCommandsForPolicy(policy), {
|
|
compactSupported,
|
|
hasCompactableContext
|
|
})
|
|
);
|
|
const localPlanAvailable =
|
|
planSupported &&
|
|
(providerSlashPolicy(policy)?.localPlanCommands.has("plan") ?? false);
|
|
// `/plan` and `/tutti` are local modifiers, not agent prompts. Drop provider
|
|
// entries that would shadow their reserved exact names, then re-surface the
|
|
// local entries when the corresponding host support exists.
|
|
const commandEntries = mergedEntries.filter((entry) => {
|
|
const commandName = normalizedCommandName(entry);
|
|
return (
|
|
!isLocalTogglePlanCommand(commandName, policy) &&
|
|
(!localPlanAvailable || !slashCommandMatchesInvocation(entry, "plan")) &&
|
|
(!tuttiSupported || !slashCommandMatchesInvocation(entry, "tutti")) &&
|
|
isSlashPaletteCommandVisible(commandName, policy)
|
|
);
|
|
});
|
|
const planEntries = localPlanAvailable ? [PLAN_MODE_COMMAND] : [];
|
|
const capabilityEntries: AgentSlashCommandCapability[] = [];
|
|
if (browserSupported) {
|
|
capabilityEntries.push(BROWSER_USE_CAPABILITY_COMMAND);
|
|
}
|
|
if (computerSupported) {
|
|
capabilityEntries.push(COMPUTER_USE_CAPABILITY_COMMAND);
|
|
}
|
|
if (tuttiSupported) {
|
|
capabilityEntries.push(TUTTI_CAPABILITY_COMMAND);
|
|
}
|
|
return [...commandEntries, ...planEntries, ...capabilityEntries];
|
|
}
|
|
|
|
export function resolveSlashCommandSelectionEffect({
|
|
provider: _provider,
|
|
policy,
|
|
command,
|
|
currentDraft
|
|
}: ResolveSlashCommandSelectionEffectInput): SlashCommandSelectionEffect | null {
|
|
if (isBrowserUseCapability(command)) {
|
|
return {
|
|
kind: "enableBrowserUse",
|
|
draft: draftForSlashCommand(command, currentDraft)
|
|
};
|
|
}
|
|
if (isComputerUseCapability(command)) {
|
|
return {
|
|
kind: "enableComputerUse",
|
|
draft: draftForSlashCommand(command, currentDraft)
|
|
};
|
|
}
|
|
if (isTuttiCapability(command)) {
|
|
return { kind: "activateTuttiMode" };
|
|
}
|
|
if (!policy) {
|
|
return {
|
|
kind: "fillDraft",
|
|
draft: draftForSlashCommand(command, currentDraft)
|
|
};
|
|
}
|
|
const commandName = normalizedCommandName(command);
|
|
if (isLocalTogglePlanCommand(commandName, policy)) {
|
|
return { kind: "enablePlanMode" };
|
|
}
|
|
if (isLocalGoalCommand(commandName, policy)) {
|
|
return { kind: "activateGoalMode" };
|
|
}
|
|
if (isLocalToggleSpeedCommand(commandName, policy)) {
|
|
return { kind: "toggleSpeed" };
|
|
}
|
|
if (isLocalStatusCommand(commandName, policy)) {
|
|
return { kind: "showStatus" };
|
|
}
|
|
// A descriptor may route a bare palette command to a local picker.
|
|
if (isReviewPickerCommand(commandName, policy)) {
|
|
return { kind: "showReviewPicker" };
|
|
}
|
|
if (isImmediateCommand(commandName, policy)) {
|
|
return {
|
|
kind: "submitPrompt",
|
|
prompt: promptForSlashCommand(command)
|
|
};
|
|
}
|
|
return {
|
|
kind: "fillDraft",
|
|
draft: draftForSlashCommand(command, currentDraft)
|
|
};
|
|
}
|
|
|
|
export function resolveSlashCommandSubmitEffect({
|
|
browserSupported = false,
|
|
computerSupported = false,
|
|
tuttiSupported = false,
|
|
provider: _provider,
|
|
policy,
|
|
commands,
|
|
draft
|
|
}: ResolveSlashCommandSubmitEffectInput): SlashCommandSelectionEffect | null {
|
|
for (const [capability, supported] of [
|
|
["browserUse", browserSupported],
|
|
["computerUse", computerSupported]
|
|
] as const) {
|
|
const capabilityEffect = resolveCapabilitySubmitEffect({
|
|
capability,
|
|
commands,
|
|
draft,
|
|
supported
|
|
});
|
|
if (capabilityEffect) {
|
|
return capabilityEffect;
|
|
}
|
|
}
|
|
const invocation = parseSlashCommandInvocation(draft);
|
|
if (!invocation) {
|
|
return null;
|
|
}
|
|
const command = commands.find((candidate) =>
|
|
slashCommandMatchesInvocation(candidate, invocation.commandName)
|
|
);
|
|
if (!command) {
|
|
return null;
|
|
}
|
|
if (isAgentSlashCommandCapability(command)) {
|
|
return tuttiSupported &&
|
|
isTuttiCapability(command) &&
|
|
invocation.args.trim() === ""
|
|
? { kind: "activateTuttiMode" }
|
|
: null;
|
|
}
|
|
const commandName = normalizedCommandName(command);
|
|
if (isLocalTogglePlanCommand(commandName, policy)) {
|
|
return invocation.args.trim() === "" ? { kind: "enablePlanMode" } : null;
|
|
}
|
|
if (isLocalGoalCommand(commandName, policy)) {
|
|
return invocation.args.trim() === "" ? { kind: "activateGoalMode" } : null;
|
|
}
|
|
if (isLocalToggleSpeedCommand(commandName, policy)) {
|
|
return { kind: "toggleSpeed" };
|
|
}
|
|
if (isLocalStatusCommand(commandName, policy)) {
|
|
return { kind: "showStatus" };
|
|
}
|
|
// Bare `/review` opens the target picker; `/review <text>` keeps the legacy
|
|
// behavior of submitting the text straight through as a custom review.
|
|
if (isReviewPickerCommand(commandName, policy)) {
|
|
return invocation.args.trim() === "" ? { kind: "showReviewPicker" } : null;
|
|
}
|
|
if (isImmediateCommand(commandName, policy)) {
|
|
return {
|
|
kind: "submitPrompt",
|
|
prompt: invocation.normalizedPrompt
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function resolveCapabilitySubmitEffect({
|
|
capability,
|
|
commands,
|
|
draft,
|
|
supported
|
|
}: {
|
|
capability: AgentCapabilityUse;
|
|
commands: readonly AgentSlashCommand[];
|
|
draft: string;
|
|
supported: boolean;
|
|
}): SlashCommandSelectionEffect | null {
|
|
if (!supported) {
|
|
return null;
|
|
}
|
|
const invocation = parseAgentCapabilityUseInvocation(draft, capability);
|
|
if (!invocation) {
|
|
return null;
|
|
}
|
|
const command = commands.find((candidate) =>
|
|
slashCommandMatchesInvocation(candidate, invocation.commandName)
|
|
);
|
|
if (!command || !isCapabilityCommand(command, capability)) {
|
|
return null;
|
|
}
|
|
return {
|
|
kind: "submitPrompt",
|
|
prompt: buildAgentCapabilityUseSubmitPrompt(capability, invocation.args),
|
|
displayPrompt: agentCapabilityUseDisplayPrompt(capability, invocation.args),
|
|
requiredSettingsPatch: { [capability]: true }
|
|
};
|
|
}
|
|
|
|
function isLocalTogglePlanCommand(
|
|
commandName: string,
|
|
policy?: AgentSlashCommandPolicy | null
|
|
): boolean {
|
|
return (
|
|
providerSlashPolicy(policy)?.localPlanCommands.has(
|
|
commandName.trim().toLowerCase()
|
|
) ?? false
|
|
);
|
|
}
|
|
|
|
function isLocalGoalCommand(
|
|
commandName: string,
|
|
policy?: AgentSlashCommandPolicy | null
|
|
): boolean {
|
|
return (
|
|
providerSlashPolicy(policy)?.localGoalCommands.has(
|
|
commandName.trim().toLowerCase()
|
|
) ?? false
|
|
);
|
|
}
|
|
|
|
function isReviewPickerCommand(
|
|
commandName: string,
|
|
policy?: AgentSlashCommandPolicy | null
|
|
): boolean {
|
|
return (
|
|
providerSlashPolicy(policy)?.reviewPickerCommands.has(commandName) ?? false
|
|
);
|
|
}
|
|
|
|
function fallbackCommandsForPolicy(
|
|
policy?: AgentSlashCommandPolicy | null
|
|
): readonly AgentSessionCommand[] {
|
|
return providerSlashPolicy(policy)?.fallbackCommands ?? [];
|
|
}
|
|
|
|
function isSlashPaletteCommandVisible(
|
|
commandName: string,
|
|
policy?: AgentSlashCommandPolicy | null
|
|
): boolean {
|
|
// AgentGUI injects /side only when the host provides the provider-neutral
|
|
// Side runtime. It is a host command and therefore is not expected to be in
|
|
// a provider's authoritative command catalog.
|
|
if (commandName === "side") {
|
|
return true;
|
|
}
|
|
if (policy?.commandCatalogAuthoritative === true) {
|
|
const fallbackCommands = Array.isArray(policy.fallbackCommands)
|
|
? policy.fallbackCommands
|
|
: [];
|
|
const commandEffects = Array.isArray(policy.commandEffects)
|
|
? policy.commandEffects
|
|
: [];
|
|
return (
|
|
fallbackCommands.some(
|
|
(command) => command.trim().toLowerCase() === commandName
|
|
) ||
|
|
commandEffects.some(
|
|
(descriptor) => descriptor.command.trim().toLowerCase() === commandName
|
|
)
|
|
);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function isLocalStatusCommand(
|
|
commandName: string,
|
|
policy?: AgentSlashCommandPolicy | null
|
|
): boolean {
|
|
return (
|
|
providerSlashPolicy(policy)?.localStatusCommands.has(commandName) ?? false
|
|
);
|
|
}
|
|
|
|
function isLocalToggleSpeedCommand(
|
|
commandName: string,
|
|
policy?: AgentSlashCommandPolicy | null
|
|
): boolean {
|
|
return (
|
|
providerSlashPolicy(policy)?.localSpeedCommands.has(commandName) ?? false
|
|
);
|
|
}
|
|
|
|
function isImmediateCommand(
|
|
commandName: string,
|
|
policy?: AgentSlashCommandPolicy | null
|
|
): boolean {
|
|
return (
|
|
providerSlashPolicy(policy)?.immediateCommands.has(commandName) ?? false
|
|
);
|
|
}
|
|
|
|
function normalizedCommandSet(values: readonly string[]): ReadonlySet<string> {
|
|
return new Set(
|
|
values.map((value) => value.trim().toLowerCase()).filter(Boolean)
|
|
);
|
|
}
|
|
|
|
export function slashCommandPolicyFromComposerOptions(
|
|
options: AgentActivityComposerOptions | null
|
|
): AgentSlashCommandPolicy | null {
|
|
return options?.slashCommandPolicy ?? null;
|
|
}
|
|
|
|
function normalizedCommandName(command: { name: string }): string {
|
|
return command.name.trim().toLowerCase();
|
|
}
|
|
|
|
function isBrowserUseCapability(
|
|
command: AgentSlashCommand
|
|
): command is AgentSlashCommandCapability {
|
|
return (
|
|
"kind" in command &&
|
|
command.kind === "capability" &&
|
|
command.capability === "browserUse"
|
|
);
|
|
}
|
|
|
|
function isComputerUseCapability(
|
|
command: AgentSlashCommand
|
|
): command is AgentSlashCommandCapability {
|
|
return (
|
|
"kind" in command &&
|
|
command.kind === "capability" &&
|
|
command.capability === "computerUse"
|
|
);
|
|
}
|
|
|
|
function isTuttiCapability(
|
|
command: AgentSlashCommand
|
|
): command is AgentSlashCommandCapability & { capability: "tutti" } {
|
|
return (
|
|
"kind" in command &&
|
|
command.kind === "capability" &&
|
|
command.capability === "tutti"
|
|
);
|
|
}
|
|
|
|
function isCapabilityCommand(
|
|
command: AgentSlashCommand,
|
|
capability: AgentSlashCommandCapability["capability"]
|
|
): command is AgentSlashCommandCapability {
|
|
return (
|
|
"kind" in command &&
|
|
command.kind === "capability" &&
|
|
command.capability === capability
|
|
);
|
|
}
|
|
|
|
function isAgentSlashCommandCapability(
|
|
command: AgentSlashCommand
|
|
): command is AgentSlashCommandCapability {
|
|
return "kind" in command && command.kind === "capability";
|
|
}
|
|
|
|
function slashCommandMatchesInvocation(
|
|
command: AgentSlashCommand,
|
|
commandName: string
|
|
): boolean {
|
|
const normalizedInvocation = commandName.trim().toLowerCase();
|
|
if (normalizedCommandName(command) === normalizedInvocation) {
|
|
return true;
|
|
}
|
|
const aliases = "aliases" in command ? (command.aliases ?? []) : [];
|
|
return aliases.some(
|
|
(alias) => alias.trim().toLowerCase() === normalizedInvocation
|
|
);
|
|
}
|
|
|
|
function filterUnavailableSlashCommands(
|
|
commands: readonly AgentSessionCommand[],
|
|
input: {
|
|
compactSupported?: boolean | null;
|
|
hasCompactableContext: boolean;
|
|
}
|
|
): AgentSessionCommand[] {
|
|
return commands.filter((command) => {
|
|
const commandName = normalizedCommandName(command);
|
|
if (commandName === "compact") {
|
|
if (input.compactSupported === false) {
|
|
return false;
|
|
}
|
|
return input.hasCompactableContext;
|
|
}
|
|
return true;
|
|
});
|
|
}
|