源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
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
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
This commit is contained in:
@@ -0,0 +1,704 @@
|
||||
import type { WorkspaceFileReference } from "@tutti-os/workspace-file-reference/contracts";
|
||||
import type {
|
||||
AgentActivityActivateSessionResult,
|
||||
AgentActivityComposerOptions,
|
||||
AgentActivitySendInputResult,
|
||||
AgentActivitySessionSettings,
|
||||
AgentActivitySnapshot,
|
||||
AgentActivityTurnCancelResponse,
|
||||
AgentPromptContentBlock
|
||||
} from "@tutti-os/agent-activity-core";
|
||||
import type {
|
||||
WorkspaceUserProject,
|
||||
WorkspaceUserProjectDefaultSelection,
|
||||
WorkspaceUserProjectMoveInput,
|
||||
WorkspaceUserProjectPathCheck,
|
||||
WorkspaceUserProjectSelectionPreparation,
|
||||
WorkspaceUserProjectSelectionPreparationInput,
|
||||
WorkspaceUserProjectServiceSnapshot
|
||||
} from "@tutti-os/workspace-user-project/contracts";
|
||||
|
||||
export const TUTTI_EXTERNAL_AT_PROVIDER_IDS = {
|
||||
agentGeneratedFile: "agent-generated-file",
|
||||
agentSession: "agent-session",
|
||||
agentTarget: "agent-target",
|
||||
file: "file",
|
||||
workspaceApp: "workspace-app",
|
||||
workspaceIssue: "workspace-issue"
|
||||
} as const;
|
||||
|
||||
export type TuttiExternalAtProviderId =
|
||||
(typeof TUTTI_EXTERNAL_AT_PROVIDER_IDS)[keyof typeof TUTTI_EXTERNAL_AT_PROVIDER_IDS];
|
||||
|
||||
export const tuttiExternalAtProviderIds = [
|
||||
TUTTI_EXTERNAL_AT_PROVIDER_IDS.file,
|
||||
TUTTI_EXTERNAL_AT_PROVIDER_IDS.workspaceIssue,
|
||||
TUTTI_EXTERNAL_AT_PROVIDER_IDS.workspaceApp,
|
||||
TUTTI_EXTERNAL_AT_PROVIDER_IDS.agentTarget,
|
||||
TUTTI_EXTERNAL_AT_PROVIDER_IDS.agentSession,
|
||||
TUTTI_EXTERNAL_AT_PROVIDER_IDS.agentGeneratedFile
|
||||
] as const satisfies readonly TuttiExternalAtProviderId[];
|
||||
|
||||
export interface TuttiExternalAtQueryInput {
|
||||
keyword: string;
|
||||
maxResults?: number;
|
||||
providers?: readonly TuttiExternalAtProviderId[];
|
||||
}
|
||||
|
||||
export interface TuttiExternalAtQueryDirectoryInput {
|
||||
/** Empty string addresses the provider-owned root directory. */
|
||||
directoryPath: string;
|
||||
maxResults?: number;
|
||||
providerId: TuttiExternalAtProviderId;
|
||||
}
|
||||
|
||||
export interface TuttiExternalAtDirectoryDescriptor {
|
||||
/** Canonical provider-owned directory path used for direct-child queries. */
|
||||
path: string;
|
||||
/** Number of direct children when the provider can determine it. */
|
||||
childCount?: number | null;
|
||||
}
|
||||
|
||||
export interface TuttiExternalAtQueryResult {
|
||||
providerId: TuttiExternalAtProviderId;
|
||||
itemId: string;
|
||||
label: string;
|
||||
subtitle?: string;
|
||||
thumbnailUrl?: string | null;
|
||||
insert: TuttiExternalAtInsertResult;
|
||||
directory?: TuttiExternalAtDirectoryDescriptor;
|
||||
}
|
||||
|
||||
export interface TuttiExternalAtResolveInput {
|
||||
providerId: TuttiExternalAtProviderId;
|
||||
entityId: string;
|
||||
scope?: Readonly<Record<string, string>>;
|
||||
}
|
||||
|
||||
export interface TuttiExternalAtResolveResult {
|
||||
label?: string;
|
||||
presentation?: TuttiExternalAtMentionPresentation;
|
||||
}
|
||||
|
||||
export interface TuttiExternalAtInvalidation {
|
||||
providerIds?: readonly TuttiExternalAtProviderId[];
|
||||
entityIds?: readonly string[];
|
||||
revision?: number;
|
||||
}
|
||||
|
||||
export interface TuttiExternalAtMentionPresentation {
|
||||
agentProviderId?: string;
|
||||
agentIconUrl?: string;
|
||||
iconUrl?: string;
|
||||
thumbnailUrl?: string;
|
||||
subtitle?: string;
|
||||
description?: string;
|
||||
participant?: string;
|
||||
status?: string;
|
||||
statusDataStatus?: string;
|
||||
statusLabel?: string;
|
||||
statusPulse?: string;
|
||||
userAvatarPlaceholderUrl?: string;
|
||||
}
|
||||
|
||||
export type TuttiExternalAtInsertResult =
|
||||
| {
|
||||
kind: "mention";
|
||||
mention: {
|
||||
entityId: string;
|
||||
label: string;
|
||||
scope?: Record<string, string>;
|
||||
presentation?: TuttiExternalAtMentionPresentation;
|
||||
};
|
||||
}
|
||||
| {
|
||||
kind: "markdown-link";
|
||||
label: string;
|
||||
href: string;
|
||||
}
|
||||
| {
|
||||
kind: "text";
|
||||
text: string;
|
||||
};
|
||||
|
||||
export interface TuttiExternalFileSelectInput {
|
||||
multiple?: boolean;
|
||||
}
|
||||
|
||||
export type TuttiExternalFileSelectResult = WorkspaceFileReference[];
|
||||
|
||||
export interface TuttiExternalFileOpenInput {
|
||||
mode?: "auto" | "preview" | "reveal";
|
||||
mtimeMs?: number | null;
|
||||
name?: string;
|
||||
path: string;
|
||||
sizeBytes?: number | null;
|
||||
}
|
||||
|
||||
export interface TuttiExternalFileUploadInput {
|
||||
purpose?: "app-asset";
|
||||
name?: string;
|
||||
mimeType?: string;
|
||||
onProgress?: (progress: TuttiExternalFileUploadProgress) => void;
|
||||
signal?: AbortSignal;
|
||||
}
|
||||
|
||||
export interface TuttiExternalUploadedFile {
|
||||
path: string;
|
||||
name: string;
|
||||
mimeType: string;
|
||||
sizeBytes: number;
|
||||
sha256: string;
|
||||
}
|
||||
|
||||
export interface TuttiExternalFileUploadProgress {
|
||||
loadedBytes: number;
|
||||
ratio: number;
|
||||
totalBytes: number;
|
||||
}
|
||||
|
||||
export const tuttiExternalManagedAiModelProviderIds = [
|
||||
"agnes",
|
||||
"openai",
|
||||
"anthropic"
|
||||
] as const;
|
||||
|
||||
export type TuttiExternalManagedAiModelProviderId =
|
||||
(typeof tuttiExternalManagedAiModelProviderIds)[number];
|
||||
|
||||
export interface TuttiExternalManagedAiModel {
|
||||
id: string;
|
||||
name?: string;
|
||||
provider: TuttiExternalManagedAiModelProviderId;
|
||||
}
|
||||
|
||||
export interface TuttiExternalPermissionRequestInput {
|
||||
permission: "managed-ai-models";
|
||||
nonce: string;
|
||||
providers?: readonly TuttiExternalManagedAiModelProviderId[];
|
||||
scopes: readonly string[];
|
||||
state: string;
|
||||
}
|
||||
|
||||
export interface TuttiExternalPermissionRequestResult {
|
||||
code: string;
|
||||
contextToken?: string;
|
||||
expiresAt?: string;
|
||||
models?: readonly TuttiExternalManagedAiModel[];
|
||||
providers?: readonly TuttiExternalManagedAiModelProviderId[];
|
||||
}
|
||||
|
||||
export interface TuttiExternalSettingsOpenInput {
|
||||
provider?: TuttiExternalManagedAiModelProviderId;
|
||||
tab?: "models";
|
||||
}
|
||||
|
||||
export interface TuttiExternalBrowserOpenUrlInput {
|
||||
url: string;
|
||||
}
|
||||
|
||||
export type TuttiExternalWorkspaceFeature =
|
||||
| "app-center"
|
||||
| "issue-manager"
|
||||
| "message-center"
|
||||
| "agent-connect"
|
||||
| "agent-chat"
|
||||
| "agent-manage";
|
||||
|
||||
export const tuttiExternalWorkspaceAgentProviders = [
|
||||
"claude-code",
|
||||
"codex",
|
||||
"cursor",
|
||||
"nexight",
|
||||
"hermes",
|
||||
"openclaw"
|
||||
] as const;
|
||||
|
||||
export type TuttiExternalWorkspaceAgentProvider =
|
||||
(typeof tuttiExternalWorkspaceAgentProviders)[number];
|
||||
|
||||
export interface TuttiExternalWorkspaceOpenFeatureInput {
|
||||
autoSubmit?: boolean;
|
||||
draftPrompt?: string;
|
||||
feature: TuttiExternalWorkspaceFeature;
|
||||
provider?: TuttiExternalWorkspaceAgentProvider;
|
||||
}
|
||||
|
||||
export interface TuttiExternalWorkspaceOpenRouteIntent {
|
||||
kind: "open-route";
|
||||
params?: Record<string, string>;
|
||||
/**
|
||||
* Origin-root path for the app launch origin. It must start with "/".
|
||||
*/
|
||||
route: string;
|
||||
state?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface TuttiExternalReferenceOpenInput {
|
||||
href: string;
|
||||
}
|
||||
|
||||
export type TuttiExternalReferenceSelection =
|
||||
| {
|
||||
selectionKind: "path";
|
||||
reference: WorkspaceFileReference;
|
||||
}
|
||||
| {
|
||||
selectionKind: "workspace-reference";
|
||||
displayName: string;
|
||||
fileCount?: number;
|
||||
groupId?: string;
|
||||
id: string;
|
||||
source: "app";
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export type TuttiExternalReferenceSelectResult =
|
||||
TuttiExternalReferenceSelection[];
|
||||
|
||||
export type TuttiExternalAgentAvailabilityStatus =
|
||||
| "ready"
|
||||
| "checking"
|
||||
| "coming_soon"
|
||||
| "not_installed"
|
||||
| "auth_required"
|
||||
| "unavailable";
|
||||
|
||||
export interface TuttiExternalAgentTarget {
|
||||
agentTargetId: string;
|
||||
availability: {
|
||||
pendingAction?: "install" | "login" | "refresh" | null;
|
||||
reason?: string | null;
|
||||
status: TuttiExternalAgentAvailabilityStatus;
|
||||
};
|
||||
description?: string | null;
|
||||
/**
|
||||
* Host-resolved presentation URL. This may be a data URL for Host-local
|
||||
* assets. Consumers must retain a fallback when the URL cannot be rendered.
|
||||
*/
|
||||
iconUrl: string;
|
||||
name: string;
|
||||
provider: string;
|
||||
}
|
||||
|
||||
export interface TuttiExternalAgentTargetCatalog {
|
||||
agents: TuttiExternalAgentTarget[];
|
||||
capturedAtUnixMs: number | null;
|
||||
error: string | null;
|
||||
status: "idle" | "loading" | "ready" | "error";
|
||||
}
|
||||
|
||||
export interface TuttiExternalAgentActivityComposerOptionsInput {
|
||||
agentTargetId: string;
|
||||
cwd?: string | null;
|
||||
provider: string;
|
||||
settings?: AgentActivitySessionSettings | null;
|
||||
}
|
||||
|
||||
export interface TuttiExternalAgentActivityActivateSessionInput {
|
||||
agentSessionId: string;
|
||||
agentTargetId: string;
|
||||
clientSubmitId: string;
|
||||
cwd?: string | null;
|
||||
initialContent: AgentPromptContentBlock[];
|
||||
initialDisplayPrompt?: string | null;
|
||||
/**
|
||||
* Host-owned navigation request. When true, the workspace owner opens its
|
||||
* Agent GUI on the activated session after activation succeeds. Missing
|
||||
* preserves the existing activate-without-navigation behavior.
|
||||
*/
|
||||
reveal?: boolean;
|
||||
settings?: AgentActivitySessionSettings;
|
||||
title?: string;
|
||||
visible?: boolean;
|
||||
}
|
||||
|
||||
export interface TuttiExternalAgentActivitySendInput {
|
||||
agentSessionId: string;
|
||||
clientSubmitId: string;
|
||||
content: AgentPromptContentBlock[];
|
||||
displayPrompt?: string | null;
|
||||
guidance?: boolean;
|
||||
}
|
||||
|
||||
export interface TuttiExternalAgentActivityCancelTurnInput {
|
||||
agentSessionId: string;
|
||||
turnId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Explicit composer picks a launcher persists into the canonical per-target
|
||||
* composer-defaults ledger. Absent fields stay untouched; null clears a field.
|
||||
*/
|
||||
export interface TuttiExternalAgentActivityRememberComposerDefaultsInput {
|
||||
agentTargetId: string;
|
||||
defaults: {
|
||||
codexSaverMode?: boolean;
|
||||
rtkSaverMode?: boolean;
|
||||
model?: string | null;
|
||||
permissionModeId?: string | null;
|
||||
reasoningEffort?: string | null;
|
||||
speed?: string | null;
|
||||
};
|
||||
}
|
||||
|
||||
export type TuttiExternalAgentActivityActivateSessionResult =
|
||||
AgentActivityActivateSessionResult;
|
||||
export type TuttiExternalAgentActivityComposerOptions =
|
||||
AgentActivityComposerOptions;
|
||||
export type TuttiExternalAgentActivitySendResult = AgentActivitySendInputResult;
|
||||
export type TuttiExternalAgentActivityCancelTurnResult =
|
||||
AgentActivityTurnCancelResponse;
|
||||
export type TuttiExternalAgentActivitySnapshot = AgentActivitySnapshot;
|
||||
|
||||
export interface TuttiExternalUserProjectCreateInput {
|
||||
name: string;
|
||||
}
|
||||
|
||||
export interface TuttiExternalUserProjectPathInput {
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface TuttiExternalUserProjectRememberDefaultSelectionInput {
|
||||
path: string | null;
|
||||
}
|
||||
|
||||
export const tuttiExternalLogLevels = [
|
||||
"debug",
|
||||
"info",
|
||||
"warn",
|
||||
"error"
|
||||
] as const;
|
||||
|
||||
export type TuttiExternalLogLevel = (typeof tuttiExternalLogLevels)[number];
|
||||
|
||||
export interface TuttiExternalLogInput {
|
||||
details?: Record<string, unknown>;
|
||||
event: string;
|
||||
level?: TuttiExternalLogLevel;
|
||||
}
|
||||
|
||||
export interface TuttiExternalPdfMargin {
|
||||
bottom?: string;
|
||||
left?: string;
|
||||
right?: string;
|
||||
top?: string;
|
||||
}
|
||||
|
||||
export type TuttiExternalPdfPageSize =
|
||||
| "A4"
|
||||
| "Letter"
|
||||
| {
|
||||
height: number;
|
||||
width: number;
|
||||
};
|
||||
|
||||
export interface TuttiExternalPdfPrintHtmlInput {
|
||||
baseUrl?: string;
|
||||
html: string;
|
||||
margin?: TuttiExternalPdfMargin;
|
||||
pageSize?: TuttiExternalPdfPageSize;
|
||||
preferCSSPageSize?: boolean;
|
||||
printBackground?: boolean;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
export interface TuttiExternalPdfPrintHtmlResult {
|
||||
bytes: Uint8Array;
|
||||
}
|
||||
|
||||
export interface TuttiExternalBridge {
|
||||
app: {
|
||||
getContext(): Promise<unknown>;
|
||||
subscribe(listener: (context: unknown) => void): () => void;
|
||||
};
|
||||
activity: {
|
||||
reportActive(): Promise<void>;
|
||||
};
|
||||
agentActivity: {
|
||||
activateSession(
|
||||
input: TuttiExternalAgentActivityActivateSessionInput
|
||||
): Promise<TuttiExternalAgentActivityActivateSessionResult>;
|
||||
cancelTurn(
|
||||
input: TuttiExternalAgentActivityCancelTurnInput
|
||||
): Promise<TuttiExternalAgentActivityCancelTurnResult>;
|
||||
getComposerOptions(
|
||||
input: TuttiExternalAgentActivityComposerOptionsInput
|
||||
): Promise<TuttiExternalAgentActivityComposerOptions>;
|
||||
getSnapshot(): Promise<TuttiExternalAgentActivitySnapshot>;
|
||||
listTargets(): Promise<TuttiExternalAgentTargetCatalog>;
|
||||
rememberComposerDefaults(
|
||||
input: TuttiExternalAgentActivityRememberComposerDefaultsInput
|
||||
): Promise<void>;
|
||||
sendInput(
|
||||
input: TuttiExternalAgentActivitySendInput
|
||||
): Promise<TuttiExternalAgentActivitySendResult>;
|
||||
};
|
||||
browser: {
|
||||
openUrl(input: TuttiExternalBrowserOpenUrlInput): Promise<void>;
|
||||
};
|
||||
at: {
|
||||
query(
|
||||
input: TuttiExternalAtQueryInput
|
||||
): Promise<TuttiExternalAtQueryResult[]>;
|
||||
queryDirectory?(
|
||||
input: TuttiExternalAtQueryDirectoryInput
|
||||
): Promise<TuttiExternalAtQueryResult[]>;
|
||||
resolve?(
|
||||
input: TuttiExternalAtResolveInput
|
||||
): Promise<TuttiExternalAtResolveResult | null>;
|
||||
subscribe?(
|
||||
listener: (event: TuttiExternalAtInvalidation) => void
|
||||
): () => void;
|
||||
};
|
||||
files: {
|
||||
select(
|
||||
input?: TuttiExternalFileSelectInput
|
||||
): Promise<TuttiExternalFileSelectResult>;
|
||||
open(input: TuttiExternalFileOpenInput): Promise<void>;
|
||||
upload(
|
||||
file: Blob | File,
|
||||
input?: TuttiExternalFileUploadInput
|
||||
): Promise<TuttiExternalUploadedFile>;
|
||||
};
|
||||
permissions: {
|
||||
request(
|
||||
input: TuttiExternalPermissionRequestInput
|
||||
): Promise<TuttiExternalPermissionRequestResult>;
|
||||
};
|
||||
settings: {
|
||||
open(input?: TuttiExternalSettingsOpenInput): Promise<void>;
|
||||
};
|
||||
workspace: {
|
||||
onLaunchIntent(
|
||||
listener: (intent: TuttiExternalWorkspaceOpenRouteIntent) => void
|
||||
): () => void;
|
||||
openFeature(input: TuttiExternalWorkspaceOpenFeatureInput): Promise<void>;
|
||||
};
|
||||
references: {
|
||||
select?(): Promise<TuttiExternalReferenceSelectResult>;
|
||||
open(input: TuttiExternalReferenceOpenInput): Promise<void>;
|
||||
};
|
||||
pdf: {
|
||||
printHtmlToPdf(
|
||||
input: TuttiExternalPdfPrintHtmlInput
|
||||
): Promise<TuttiExternalPdfPrintHtmlResult>;
|
||||
};
|
||||
userProjects: {
|
||||
checkPath(
|
||||
input: TuttiExternalUserProjectPathInput
|
||||
): Promise<WorkspaceUserProjectPathCheck>;
|
||||
create(
|
||||
input: TuttiExternalUserProjectCreateInput
|
||||
): Promise<WorkspaceUserProject>;
|
||||
getDefaultSelection(): Promise<WorkspaceUserProjectDefaultSelection | null>;
|
||||
getSnapshot(): Promise<WorkspaceUserProjectServiceSnapshot>;
|
||||
list(): Promise<{ projects: WorkspaceUserProject[] }>;
|
||||
move(input: WorkspaceUserProjectMoveInput): Promise<void>;
|
||||
remove(input: TuttiExternalUserProjectPathInput): Promise<void>;
|
||||
prepareSelection(
|
||||
input: WorkspaceUserProjectSelectionPreparationInput
|
||||
): Promise<WorkspaceUserProjectSelectionPreparation>;
|
||||
refresh(): Promise<WorkspaceUserProjectServiceSnapshot>;
|
||||
rememberDefaultSelection(
|
||||
input: TuttiExternalUserProjectRememberDefaultSelectionInput
|
||||
): Promise<void>;
|
||||
selectDirectory(): Promise<{ path: string } | null>;
|
||||
/**
|
||||
* Subscribes to user-project snapshots. Implementations replay the latest
|
||||
* known snapshot after registration, then emit future snapshots.
|
||||
*/
|
||||
subscribe(
|
||||
listener: (snapshot: WorkspaceUserProjectServiceSnapshot) => void
|
||||
): () => void;
|
||||
use(
|
||||
input: TuttiExternalUserProjectPathInput
|
||||
): Promise<WorkspaceUserProject>;
|
||||
};
|
||||
logs: {
|
||||
write(input: TuttiExternalLogInput): void;
|
||||
};
|
||||
}
|
||||
|
||||
export type TuttiExternalRendererRequest =
|
||||
| {
|
||||
appId: string;
|
||||
input: TuttiExternalAgentActivityActivateSessionInput;
|
||||
operation: "agentActivity.activateSession";
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
| {
|
||||
appId: string;
|
||||
input: TuttiExternalAgentActivityCancelTurnInput;
|
||||
operation: "agentActivity.cancelTurn";
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
| {
|
||||
appId: string;
|
||||
input: TuttiExternalAgentActivityComposerOptionsInput;
|
||||
operation: "agentActivity.getComposerOptions";
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
| {
|
||||
appId: string;
|
||||
operation: "agentActivity.getSnapshot";
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
| {
|
||||
appId: string;
|
||||
operation: "agentActivity.listTargets";
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
| {
|
||||
appId: string;
|
||||
input: TuttiExternalAgentActivityRememberComposerDefaultsInput;
|
||||
operation: "agentActivity.rememberComposerDefaults";
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
| {
|
||||
appId: string;
|
||||
input: TuttiExternalAgentActivitySendInput;
|
||||
operation: "agentActivity.sendInput";
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
| {
|
||||
appId: string;
|
||||
input: TuttiExternalAtResolveInput;
|
||||
operation: "at.resolve";
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
| {
|
||||
appId: string;
|
||||
input: TuttiExternalAtQueryInput;
|
||||
operation: "at.query";
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
| {
|
||||
appId: string;
|
||||
input: TuttiExternalAtQueryDirectoryInput;
|
||||
operation: "at.queryDirectory";
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
| {
|
||||
appId: string;
|
||||
input: TuttiExternalFileSelectInput;
|
||||
operation: "files.select";
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
| {
|
||||
appId: string;
|
||||
input: TuttiExternalFileOpenInput;
|
||||
operation: "files.open";
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
| {
|
||||
appId: string;
|
||||
input: TuttiExternalSettingsOpenInput;
|
||||
operation: "settings.open";
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
| {
|
||||
appId: string;
|
||||
input: TuttiExternalReferenceOpenInput;
|
||||
operation: "references.open";
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
| {
|
||||
appId: string;
|
||||
operation: "references.select";
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
| {
|
||||
appId: string;
|
||||
input: TuttiExternalUserProjectPathInput;
|
||||
operation: "userProjects.checkPath";
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
| {
|
||||
appId: string;
|
||||
input: TuttiExternalUserProjectCreateInput;
|
||||
operation: "userProjects.create";
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
| {
|
||||
appId: string;
|
||||
operation: "userProjects.getDefaultSelection";
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
| {
|
||||
appId: string;
|
||||
operation: "userProjects.getSnapshot";
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
| {
|
||||
appId: string;
|
||||
operation: "userProjects.list";
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
| {
|
||||
appId: string;
|
||||
input: WorkspaceUserProjectMoveInput;
|
||||
operation: "userProjects.move";
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
| {
|
||||
appId: string;
|
||||
input: TuttiExternalUserProjectPathInput;
|
||||
operation: "userProjects.remove";
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
| {
|
||||
appId: string;
|
||||
input: WorkspaceUserProjectSelectionPreparationInput;
|
||||
operation: "userProjects.prepareSelection";
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
| {
|
||||
appId: string;
|
||||
operation: "userProjects.refresh";
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
| {
|
||||
appId: string;
|
||||
input: TuttiExternalUserProjectRememberDefaultSelectionInput;
|
||||
operation: "userProjects.rememberDefaultSelection";
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
| {
|
||||
appId: string;
|
||||
operation: "userProjects.selectDirectory";
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
}
|
||||
| {
|
||||
appId: string;
|
||||
input: TuttiExternalUserProjectPathInput;
|
||||
operation: "userProjects.use";
|
||||
requestId: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
@@ -0,0 +1,779 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {
|
||||
normalizeTuttiExternalAtQueryInput,
|
||||
normalizeTuttiExternalAtQueryDirectoryInput,
|
||||
normalizeTuttiExternalAtResolveInput,
|
||||
normalizeTuttiExternalAtInvalidation,
|
||||
normalizeTuttiExternalAgentActivityActivateSessionInput,
|
||||
normalizeTuttiExternalAgentActivityCancelTurnInput,
|
||||
normalizeTuttiExternalAgentActivityRememberComposerDefaultsInput,
|
||||
normalizeTuttiExternalAgentActivityComposerOptionsInput,
|
||||
normalizeTuttiExternalAgentActivitySendInput,
|
||||
normalizeTuttiExternalBrowserOpenUrlInput,
|
||||
normalizeTuttiExternalFileOpenInput,
|
||||
normalizeTuttiExternalFileSelectInput,
|
||||
normalizeTuttiExternalFileUploadInput,
|
||||
normalizeTuttiExternalLogInput,
|
||||
normalizeTuttiExternalPdfPrintHtmlInput,
|
||||
normalizeTuttiExternalPermissionRequestInput,
|
||||
normalizeTuttiExternalReferenceOpenInput,
|
||||
normalizeTuttiExternalSettingsOpenInput,
|
||||
normalizeTuttiExternalUserProjectCreateInput,
|
||||
normalizeTuttiExternalUserProjectMoveInput,
|
||||
normalizeTuttiExternalUserProjectPathInput,
|
||||
normalizeTuttiExternalUserProjectRememberDefaultSelectionInput,
|
||||
normalizeTuttiExternalUserProjectSelectionPreparationInput,
|
||||
normalizeTuttiExternalWorkspaceOpenFeatureInput,
|
||||
tuttiExternalAtDefaultMaxResults,
|
||||
tuttiExternalAtMaxResultsLimit
|
||||
} from "./index.ts";
|
||||
|
||||
test("normalizes at query defaults", () => {
|
||||
assert.deepEqual(normalizeTuttiExternalAtQueryInput({ keyword: "readme" }), {
|
||||
keyword: "readme",
|
||||
maxResults: tuttiExternalAtDefaultMaxResults,
|
||||
providers: undefined
|
||||
});
|
||||
});
|
||||
|
||||
test("caps at query max results and deduplicates providers", () => {
|
||||
assert.deepEqual(
|
||||
normalizeTuttiExternalAtQueryInput({
|
||||
keyword: "",
|
||||
maxResults: tuttiExternalAtMaxResultsLimit + 10,
|
||||
providers: ["file", "file", "agent-session"]
|
||||
}),
|
||||
{
|
||||
keyword: "",
|
||||
maxResults: tuttiExternalAtMaxResultsLimit,
|
||||
providers: ["file", "agent-session"]
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizes external at directory queries including the provider root", () => {
|
||||
assert.deepEqual(
|
||||
normalizeTuttiExternalAtQueryDirectoryInput({
|
||||
directoryPath: " ",
|
||||
maxResults: tuttiExternalAtMaxResultsLimit + 10,
|
||||
providerId: "file"
|
||||
}),
|
||||
{
|
||||
directoryPath: "",
|
||||
maxResults: tuttiExternalAtMaxResultsLimit,
|
||||
providerId: "file"
|
||||
}
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeTuttiExternalAtQueryDirectoryInput({
|
||||
directoryPath: "",
|
||||
providerId: "unknown"
|
||||
}),
|
||||
/providerId is unsupported/
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects unsupported at providers", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeTuttiExternalAtQueryInput({
|
||||
keyword: "readme",
|
||||
providers: ["file", "not-supported"]
|
||||
}),
|
||||
/unsupported provider/
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizes exact at resolve identity and invalidation", () => {
|
||||
assert.deepEqual(
|
||||
normalizeTuttiExternalAtResolveInput({
|
||||
providerId: "workspace-app",
|
||||
entityId: " canvas ",
|
||||
scope: { workspaceId: "workspace-1" }
|
||||
}),
|
||||
{
|
||||
providerId: "workspace-app",
|
||||
entityId: "canvas",
|
||||
scope: { workspaceId: "workspace-1" }
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
normalizeTuttiExternalAtInvalidation({
|
||||
providerIds: ["workspace-app", "workspace-app"],
|
||||
entityIds: [" canvas ", "canvas"],
|
||||
revision: 2
|
||||
}),
|
||||
{
|
||||
providerIds: ["workspace-app"],
|
||||
entityIds: ["canvas"],
|
||||
revision: 2
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects malformed at resolve identity and invalidation", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeTuttiExternalAtResolveInput({
|
||||
providerId: "unknown",
|
||||
entityId: "canvas"
|
||||
}),
|
||||
/providerId is unsupported/
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeTuttiExternalAtResolveInput({
|
||||
providerId: "workspace-app",
|
||||
entityId: "canvas",
|
||||
scope: { workspaceId: 1 }
|
||||
}),
|
||||
/scope must contain string values/
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeTuttiExternalAtInvalidation({ revision: Number.NaN }),
|
||||
/revision must be finite/
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizes agent activity session inputs without dropping prompt assets", () => {
|
||||
assert.deepEqual(
|
||||
normalizeTuttiExternalAgentActivityActivateSessionInput({
|
||||
agentSessionId: " session-1 ",
|
||||
agentTargetId: " codex ",
|
||||
clientSubmitId: " batch-1 ",
|
||||
cwd: " /workspace/project ",
|
||||
initialContent: [
|
||||
{ type: "text", text: "Run the provider smoke test." },
|
||||
{
|
||||
type: "file",
|
||||
name: "fixture.txt",
|
||||
path: "fixtures/fixture.txt",
|
||||
sizeBytes: 42
|
||||
}
|
||||
],
|
||||
initialDisplayPrompt: " provider smoke test ",
|
||||
reveal: true,
|
||||
settings: {
|
||||
browserUse: false,
|
||||
model: " test-model ",
|
||||
planMode: null
|
||||
},
|
||||
title: " Provider Core Lab ",
|
||||
visible: true
|
||||
}),
|
||||
{
|
||||
agentSessionId: "session-1",
|
||||
agentTargetId: "codex",
|
||||
clientSubmitId: "batch-1",
|
||||
cwd: "/workspace/project",
|
||||
initialContent: [
|
||||
{ type: "text", text: "Run the provider smoke test." },
|
||||
{
|
||||
type: "file",
|
||||
name: "fixture.txt",
|
||||
path: "fixtures/fixture.txt",
|
||||
sizeBytes: 42
|
||||
}
|
||||
],
|
||||
initialDisplayPrompt: "provider smoke test",
|
||||
reveal: true,
|
||||
settings: {
|
||||
browserUse: false,
|
||||
model: "test-model",
|
||||
planMode: null
|
||||
},
|
||||
title: "Provider Core Lab",
|
||||
visible: true
|
||||
}
|
||||
);
|
||||
assert.equal(
|
||||
"reveal" in
|
||||
normalizeTuttiExternalAgentActivityActivateSessionInput({
|
||||
agentSessionId: "session-1",
|
||||
agentTargetId: "codex",
|
||||
clientSubmitId: "batch-1",
|
||||
initialContent: [{ type: "text", text: "test" }]
|
||||
}),
|
||||
false
|
||||
);
|
||||
assert.deepEqual(
|
||||
normalizeTuttiExternalAgentActivitySendInput({
|
||||
agentSessionId: " session-1 ",
|
||||
clientSubmitId: " turn-2 ",
|
||||
content: [{ type: "image", data: "base64", mimeType: "image/png" }],
|
||||
displayPrompt: " image assertion ",
|
||||
guidance: false
|
||||
}),
|
||||
{
|
||||
agentSessionId: "session-1",
|
||||
clientSubmitId: "turn-2",
|
||||
content: [{ type: "image", data: "base64", mimeType: "image/png" }],
|
||||
displayPrompt: "image assertion",
|
||||
guidance: false
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizes remember-composer-defaults input", () => {
|
||||
assert.deepEqual(
|
||||
normalizeTuttiExternalAgentActivityRememberComposerDefaultsInput({
|
||||
agentTargetId: " local:codex ",
|
||||
defaults: {
|
||||
codexSaverMode: false,
|
||||
model: " gpt-5.6-sol ",
|
||||
permissionModeId: null,
|
||||
reasoningEffort: " ",
|
||||
speed: undefined
|
||||
}
|
||||
}),
|
||||
{
|
||||
agentTargetId: "local:codex",
|
||||
defaults: {
|
||||
codexSaverMode: false,
|
||||
model: "gpt-5.6-sol",
|
||||
permissionModeId: null,
|
||||
reasoningEffort: null
|
||||
}
|
||||
}
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeTuttiExternalAgentActivityRememberComposerDefaultsInput({
|
||||
agentTargetId: "local:codex",
|
||||
defaults: { model: 5 }
|
||||
}),
|
||||
/model must be a string or null/
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeTuttiExternalAgentActivityRememberComposerDefaultsInput({
|
||||
agentTargetId: "local:codex"
|
||||
}),
|
||||
/defaults must be an object/
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizes agent activity target inputs", () => {
|
||||
assert.deepEqual(
|
||||
normalizeTuttiExternalAgentActivityComposerOptionsInput({
|
||||
agentTargetId: " codex ",
|
||||
cwd: null,
|
||||
provider: " codex ",
|
||||
settings: null
|
||||
}),
|
||||
{
|
||||
agentTargetId: "codex",
|
||||
cwd: null,
|
||||
provider: "codex"
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
normalizeTuttiExternalAgentActivityCancelTurnInput({
|
||||
agentSessionId: " session-1 ",
|
||||
turnId: " turn-1 "
|
||||
}),
|
||||
{ agentSessionId: "session-1", turnId: "turn-1" }
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects malformed agent activity inputs", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeTuttiExternalAgentActivityActivateSessionInput({
|
||||
agentSessionId: "session-1",
|
||||
agentTargetId: "codex",
|
||||
clientSubmitId: "batch-1",
|
||||
initialContent: [],
|
||||
visible: true
|
||||
}),
|
||||
/initialContent must be a non-empty array/
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeTuttiExternalAgentActivitySendInput({
|
||||
agentSessionId: "session-1",
|
||||
clientSubmitId: "turn-2",
|
||||
content: [{ type: "file", sizeBytes: -1 }]
|
||||
}),
|
||||
/sizeBytes must be a non-negative number/
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeTuttiExternalAgentActivityActivateSessionInput({
|
||||
agentSessionId: "session-1",
|
||||
agentTargetId: "codex",
|
||||
clientSubmitId: "batch-1",
|
||||
initialContent: [{ type: "text", text: "test" }],
|
||||
visible: "yes"
|
||||
}),
|
||||
/visible must be a boolean/
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeTuttiExternalAgentActivityActivateSessionInput({
|
||||
agentSessionId: "session-1",
|
||||
agentTargetId: "codex",
|
||||
clientSubmitId: "batch-1",
|
||||
initialContent: [{ type: "text", text: "test" }],
|
||||
reveal: "yes"
|
||||
}),
|
||||
/reveal must be a boolean/
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizes browser open URL input", () => {
|
||||
assert.deepEqual(
|
||||
normalizeTuttiExternalBrowserOpenUrlInput({
|
||||
url: " https://example.com/design "
|
||||
}),
|
||||
{
|
||||
url: "https://example.com/design"
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects invalid browser open URL input", () => {
|
||||
assert.throws(
|
||||
() => normalizeTuttiExternalBrowserOpenUrlInput({ url: "" }),
|
||||
/browser\.openUrl url is required/
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeTuttiExternalBrowserOpenUrlInput({ url: "file:///tmp/a.html" }),
|
||||
/browser\.openUrl protocol is unsupported/
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizes file select input", () => {
|
||||
assert.deepEqual(normalizeTuttiExternalFileSelectInput(undefined), {});
|
||||
assert.deepEqual(normalizeTuttiExternalFileSelectInput({ multiple: true }), {
|
||||
multiple: true
|
||||
});
|
||||
assert.deepEqual(normalizeTuttiExternalFileSelectInput({ multiple: false }), {
|
||||
multiple: false
|
||||
});
|
||||
});
|
||||
|
||||
test("normalizes file open input", () => {
|
||||
assert.deepEqual(
|
||||
normalizeTuttiExternalFileOpenInput({
|
||||
mode: "auto",
|
||||
mtimeMs: 123,
|
||||
name: " Report.md ",
|
||||
path: " docs/report.md ",
|
||||
sizeBytes: null
|
||||
}),
|
||||
{
|
||||
mode: "auto",
|
||||
mtimeMs: 123,
|
||||
name: "Report.md",
|
||||
path: "docs/report.md",
|
||||
sizeBytes: null
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects invalid file open input", () => {
|
||||
assert.throws(
|
||||
() => normalizeTuttiExternalFileOpenInput({ path: "" }),
|
||||
/path is required/
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeTuttiExternalFileOpenInput({ path: "README.md", mode: "x" }),
|
||||
/mode is unsupported/
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizes file upload input defaults", () => {
|
||||
assert.deepEqual(normalizeTuttiExternalFileUploadInput(undefined), {
|
||||
purpose: "app-asset"
|
||||
});
|
||||
});
|
||||
|
||||
test("normalizes file upload input strings", () => {
|
||||
const controller = new AbortController();
|
||||
const onProgress = () => undefined;
|
||||
assert.deepEqual(
|
||||
normalizeTuttiExternalFileUploadInput({
|
||||
purpose: "app-asset",
|
||||
name: " Image.PNG ",
|
||||
mimeType: " image/png ",
|
||||
onProgress,
|
||||
signal: controller.signal
|
||||
}),
|
||||
{
|
||||
purpose: "app-asset",
|
||||
name: "Image.PNG",
|
||||
mimeType: "image/png",
|
||||
onProgress,
|
||||
signal: controller.signal
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects invalid file upload input", () => {
|
||||
assert.throws(
|
||||
() => normalizeTuttiExternalFileUploadInput("bad"),
|
||||
/input must be an object/
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeTuttiExternalFileUploadInput({ purpose: "avatar" }),
|
||||
/purpose is unsupported/
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeTuttiExternalFileUploadInput({ name: 1 }),
|
||||
/name must be a string/
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeTuttiExternalFileUploadInput({ mimeType: 1 }),
|
||||
/mimeType must be a string/
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeTuttiExternalFileUploadInput({ onProgress: true }),
|
||||
/onProgress must be a function/
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeTuttiExternalFileUploadInput({ signal: {} }),
|
||||
/signal must be an AbortSignal/
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizes managed AI model permission requests", () => {
|
||||
assert.deepEqual(
|
||||
normalizeTuttiExternalPermissionRequestInput({
|
||||
nonce: " nonce-1 ",
|
||||
permission: "managed-ai-models",
|
||||
providers: ["openai", "openai", "anthropic"],
|
||||
scopes: [" model:invoke ", "model:invoke"],
|
||||
state: " state-1 "
|
||||
}),
|
||||
{
|
||||
nonce: "nonce-1",
|
||||
permission: "managed-ai-models",
|
||||
providers: ["openai", "anthropic"],
|
||||
scopes: ["model:invoke"],
|
||||
state: "state-1"
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects invalid managed AI model permission requests", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeTuttiExternalPermissionRequestInput({
|
||||
nonce: "nonce-1",
|
||||
permission: "managed-credentials",
|
||||
scopes: ["model:invoke"],
|
||||
state: "state-1"
|
||||
}),
|
||||
/permission is unsupported/
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeTuttiExternalPermissionRequestInput({
|
||||
nonce: "nonce-1",
|
||||
permission: "managed-ai-models",
|
||||
scopes: [],
|
||||
state: "state-1"
|
||||
}),
|
||||
/scopes must not be empty/
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeTuttiExternalPermissionRequestInput({
|
||||
nonce: "nonce-1",
|
||||
permission: "managed-ai-models",
|
||||
providers: ["not-supported"],
|
||||
scopes: ["model:invoke"],
|
||||
state: "state-1"
|
||||
}),
|
||||
/provider is unsupported/
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizes settings open input", () => {
|
||||
assert.deepEqual(normalizeTuttiExternalSettingsOpenInput(undefined), {});
|
||||
assert.deepEqual(
|
||||
normalizeTuttiExternalSettingsOpenInput({
|
||||
provider: "openai",
|
||||
tab: "models"
|
||||
}),
|
||||
{
|
||||
provider: "openai",
|
||||
tab: "models"
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects invalid settings open input", () => {
|
||||
assert.throws(
|
||||
() => normalizeTuttiExternalSettingsOpenInput({ tab: "credentials" }),
|
||||
/tab is unsupported/
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeTuttiExternalSettingsOpenInput({ provider: "codex" }),
|
||||
/provider is unsupported/
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizes PDF print HTML input", () => {
|
||||
assert.deepEqual(
|
||||
normalizeTuttiExternalPdfPrintHtmlInput({
|
||||
baseUrl: " http://127.0.0.1:8790/project/ ",
|
||||
html: " <h1>Doc</h1> ",
|
||||
margin: {
|
||||
bottom: "14mm",
|
||||
left: " 10mm ",
|
||||
right: "10mm",
|
||||
top: "12mm"
|
||||
},
|
||||
pageSize: { width: 13.333333, height: 7.5 },
|
||||
preferCSSPageSize: true,
|
||||
printBackground: false,
|
||||
title: " Report "
|
||||
}),
|
||||
{
|
||||
baseUrl: "http://127.0.0.1:8790/project/",
|
||||
html: "<h1>Doc</h1>",
|
||||
margin: {
|
||||
bottom: "14mm",
|
||||
left: "10mm",
|
||||
right: "10mm",
|
||||
top: "12mm"
|
||||
},
|
||||
pageSize: { width: 13.333333, height: 7.5 },
|
||||
preferCSSPageSize: true,
|
||||
printBackground: false,
|
||||
title: "Report"
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects invalid PDF print HTML input", () => {
|
||||
assert.throws(
|
||||
() => normalizeTuttiExternalPdfPrintHtmlInput({ html: "" }),
|
||||
/html is required/
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeTuttiExternalPdfPrintHtmlInput({
|
||||
baseUrl: "file:///tmp/doc.html",
|
||||
html: "<h1>Doc</h1>"
|
||||
}),
|
||||
/baseUrl protocol is unsupported/
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeTuttiExternalPdfPrintHtmlInput({
|
||||
html: "<h1>Doc</h1>",
|
||||
margin: { top: "12pt" }
|
||||
}),
|
||||
/margin top unit is unsupported/
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeTuttiExternalPdfPrintHtmlInput({
|
||||
html: "<h1>Doc</h1>",
|
||||
pageSize: { width: 13.333333, height: 0 }
|
||||
}),
|
||||
/pageSize height must be a positive number/
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizes workspace feature open input", () => {
|
||||
assert.deepEqual(
|
||||
normalizeTuttiExternalWorkspaceOpenFeatureInput({
|
||||
feature: "agent-manage",
|
||||
provider: " codex "
|
||||
}),
|
||||
{
|
||||
feature: "agent-manage",
|
||||
provider: "codex"
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects invalid workspace feature open input", () => {
|
||||
assert.throws(
|
||||
() => normalizeTuttiExternalWorkspaceOpenFeatureInput(null),
|
||||
/input must be an object/
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeTuttiExternalWorkspaceOpenFeatureInput({
|
||||
feature: "not-supported"
|
||||
}),
|
||||
/feature is unsupported/
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeTuttiExternalWorkspaceOpenFeatureInput({
|
||||
feature: "agent-chat",
|
||||
provider: "unknown-agent"
|
||||
}),
|
||||
/provider is unsupported/
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizes reference open input", () => {
|
||||
assert.deepEqual(
|
||||
normalizeTuttiExternalReferenceOpenInput({
|
||||
href: "mention://workspace-app/app-1?workspaceId=workspace-1"
|
||||
}),
|
||||
{
|
||||
href: "mention://workspace-app/app-1?workspaceId=workspace-1"
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects invalid reference open input", () => {
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeTuttiExternalReferenceOpenInput({ href: "https://example.com" }),
|
||||
/mention URL/
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizes user project inputs", () => {
|
||||
assert.deepEqual(
|
||||
normalizeTuttiExternalUserProjectCreateInput({ name: " Project " }),
|
||||
{
|
||||
name: "Project"
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
normalizeTuttiExternalUserProjectPathInput({ path: " /repo " }, "use"),
|
||||
{
|
||||
path: "/repo"
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
normalizeTuttiExternalUserProjectPathInput({ path: " /repo " }, "remove"),
|
||||
{
|
||||
path: "/repo"
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
normalizeTuttiExternalUserProjectMoveInput({
|
||||
beforeProjectId: " before ",
|
||||
projectId: " project "
|
||||
}),
|
||||
{ beforeProjectId: "before", projectId: "project" }
|
||||
);
|
||||
assert.deepEqual(
|
||||
normalizeTuttiExternalUserProjectMoveInput({
|
||||
beforeProjectId: null,
|
||||
projectId: "project"
|
||||
}),
|
||||
{ beforeProjectId: null, projectId: "project" }
|
||||
);
|
||||
assert.deepEqual(
|
||||
normalizeTuttiExternalUserProjectRememberDefaultSelectionInput({
|
||||
path: " "
|
||||
}),
|
||||
{
|
||||
path: null
|
||||
}
|
||||
);
|
||||
assert.deepEqual(
|
||||
normalizeTuttiExternalUserProjectSelectionPreparationInput({
|
||||
projectLocked: true,
|
||||
selectedPath: " /repo "
|
||||
}),
|
||||
{
|
||||
projectLocked: true,
|
||||
selectedPath: "/repo"
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("rejects invalid user project inputs", () => {
|
||||
assert.throws(
|
||||
() => normalizeTuttiExternalUserProjectCreateInput({ name: "" }),
|
||||
/name is required/
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeTuttiExternalUserProjectPathInput({ path: "" }, "checkPath"),
|
||||
/path is required/
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeTuttiExternalUserProjectMoveInput({ projectId: "project" }),
|
||||
/beforeProjectId is required/
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeTuttiExternalUserProjectMoveInput({
|
||||
beforeProjectId: "",
|
||||
projectId: "project"
|
||||
}),
|
||||
/beforeProjectId is required/
|
||||
);
|
||||
});
|
||||
|
||||
test("normalizes external log input defaults", () => {
|
||||
assert.deepEqual(normalizeTuttiExternalLogInput({ event: "page.loaded" }), {
|
||||
event: "page.loaded"
|
||||
});
|
||||
});
|
||||
|
||||
test("normalizes external log input with level and details", () => {
|
||||
assert.deepEqual(
|
||||
normalizeTuttiExternalLogInput({
|
||||
event: " request.failed ",
|
||||
level: "warn",
|
||||
details: {
|
||||
route: "/home",
|
||||
retryCount: 2
|
||||
}
|
||||
}),
|
||||
{
|
||||
event: "request.failed",
|
||||
level: "warn",
|
||||
details: {
|
||||
route: "/home",
|
||||
retryCount: 2
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
test("truncates long external log detail strings", () => {
|
||||
const longValue = "x".repeat(8_500);
|
||||
const normalized = normalizeTuttiExternalLogInput({
|
||||
event: "payload.large",
|
||||
details: {
|
||||
message: longValue
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(normalized.details?.message, `${"x".repeat(8_000)}...`);
|
||||
});
|
||||
|
||||
test("rejects invalid external log input", () => {
|
||||
assert.throws(
|
||||
() => normalizeTuttiExternalLogInput(null),
|
||||
/input must be an object/
|
||||
);
|
||||
assert.throws(
|
||||
() => normalizeTuttiExternalLogInput({ event: "" }),
|
||||
/event is required/
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeTuttiExternalLogInput({
|
||||
event: "page.loaded",
|
||||
level: "trace"
|
||||
}),
|
||||
/level is unsupported/
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
normalizeTuttiExternalLogInput({
|
||||
event: "page.loaded",
|
||||
details: "invalid"
|
||||
}),
|
||||
/details must be an object/
|
||||
);
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,3 @@
|
||||
export type * from "./contracts/index.ts";
|
||||
export * from "./core/index.ts";
|
||||
export * from "./rich-text/index.ts";
|
||||
@@ -0,0 +1,494 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {
|
||||
appendTuttiExternalReferenceSelections,
|
||||
createTuttiExternalAtRichTextTriggerProvider,
|
||||
createTuttiExternalAtRichTextTriggerProviders,
|
||||
createTuttiExternalRichTextMentionService,
|
||||
queryTuttiExternalAtRichTextTriggerItems
|
||||
} from "./index.ts";
|
||||
import type {
|
||||
TuttiExternalAtQueryInput,
|
||||
TuttiExternalAtQueryResult
|
||||
} from "../contracts/index.ts";
|
||||
|
||||
test("appends selected paths and application reference bundles", () => {
|
||||
assert.equal(
|
||||
appendTuttiExternalReferenceSelections("Create a summary", [
|
||||
{
|
||||
selectionKind: "path",
|
||||
reference: {
|
||||
displayName: "notes.md",
|
||||
kind: "file",
|
||||
path: "/workspace/notes.md"
|
||||
}
|
||||
},
|
||||
{
|
||||
selectionKind: "path",
|
||||
reference: {
|
||||
displayName: "Sources",
|
||||
kind: "folder",
|
||||
path: "/workspace/sources"
|
||||
}
|
||||
},
|
||||
{
|
||||
selectionKind: "workspace-reference",
|
||||
displayName: "Canvas outputs",
|
||||
fileCount: 3,
|
||||
groupId: "outputs",
|
||||
id: "ai-canvas",
|
||||
source: "app",
|
||||
workspaceId: "workspace-1"
|
||||
}
|
||||
]),
|
||||
"Create a summary [notes.md](/workspace/notes.md) [Sources](/workspace/sources/) [@Canvas outputs](mention://workspace-reference/ai-canvas?count=3&groupId=outputs&source=app&workspaceId=workspace-1)"
|
||||
);
|
||||
});
|
||||
|
||||
test("deduplicates existing selected paths and application reference bundles", () => {
|
||||
const content =
|
||||
"Use [notes.md](/workspace/notes.md) [@Canvas outputs](mention://workspace-reference/ai-canvas?count=2&groupId=outputs&source=app&workspaceId=workspace-1)";
|
||||
assert.equal(
|
||||
appendTuttiExternalReferenceSelections(content, [
|
||||
{
|
||||
selectionKind: "path",
|
||||
reference: {
|
||||
displayName: "notes.md",
|
||||
kind: "file",
|
||||
path: "/workspace/notes.md"
|
||||
}
|
||||
},
|
||||
{
|
||||
selectionKind: "workspace-reference",
|
||||
displayName: "Canvas outputs",
|
||||
fileCount: 3,
|
||||
groupId: "outputs",
|
||||
id: "ai-canvas",
|
||||
source: "app",
|
||||
workspaceId: "workspace-1"
|
||||
}
|
||||
]),
|
||||
content
|
||||
);
|
||||
});
|
||||
|
||||
test("creates one rich text provider per requested external at provider", () => {
|
||||
const providers = createTuttiExternalAtRichTextTriggerProviders({
|
||||
bridge: null,
|
||||
providerIds: ["workspace-app", "agent-target", "agent-session"]
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
providers.map((provider) => provider.id),
|
||||
["workspace-app", "agent-target", "agent-session"]
|
||||
);
|
||||
assert.deepEqual(
|
||||
providers.map((provider) => provider.trigger),
|
||||
["@", "@", "@"]
|
||||
);
|
||||
});
|
||||
|
||||
test("queries the external bridge with the provider filter", async () => {
|
||||
const calls: TuttiExternalAtQueryInput[] = [];
|
||||
const provider = createTuttiExternalAtRichTextTriggerProvider({
|
||||
providerId: "workspace-app",
|
||||
bridge: {
|
||||
at: {
|
||||
query(input) {
|
||||
calls.push(input);
|
||||
return [
|
||||
createQueryResult("workspace-app", "apps", "Apps"),
|
||||
createQueryResult("agent-session", "session", "Session")
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const results = await provider.query({
|
||||
keyword: "app",
|
||||
maxResults: 5,
|
||||
context: {},
|
||||
trigger: "@"
|
||||
});
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
{
|
||||
keyword: "app",
|
||||
maxResults: 5,
|
||||
providers: ["workspace-app"]
|
||||
}
|
||||
]);
|
||||
assert.deepEqual(
|
||||
results.map((item) => item.providerId),
|
||||
["workspace-app"]
|
||||
);
|
||||
});
|
||||
|
||||
test("file provider exposes external directory browsing when the host supports it", async () => {
|
||||
const calls: unknown[] = [];
|
||||
const directory = createQueryResult("file", "/workspace/docs", "docs", {
|
||||
directory: { childCount: 2, path: "/workspace/docs" },
|
||||
insert: {
|
||||
href: "/workspace/docs/",
|
||||
kind: "markdown-link",
|
||||
label: "docs"
|
||||
}
|
||||
});
|
||||
const provider = createTuttiExternalAtRichTextTriggerProvider({
|
||||
providerId: "file",
|
||||
bridge: {
|
||||
at: {
|
||||
query: () => [],
|
||||
queryDirectory(input) {
|
||||
calls.push(input);
|
||||
return [directory];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert.deepEqual(provider.getItemDirectory?.(directory), directory.directory);
|
||||
assert.deepEqual(
|
||||
await provider.queryDirectory?.({
|
||||
context: {},
|
||||
directoryPath: "/workspace",
|
||||
keyword: "",
|
||||
maxResults: 12,
|
||||
trigger: "@"
|
||||
}),
|
||||
[directory]
|
||||
);
|
||||
assert.deepEqual(calls, [
|
||||
{
|
||||
directoryPath: "/workspace",
|
||||
maxResults: 12,
|
||||
providerId: "file"
|
||||
}
|
||||
]);
|
||||
});
|
||||
|
||||
test("file provider keeps directory browsing absent on an older bridge", () => {
|
||||
const provider = createTuttiExternalAtRichTextTriggerProvider({
|
||||
providerId: "file",
|
||||
bridge: { at: { query: () => [] } }
|
||||
});
|
||||
assert.equal(provider.queryDirectory, undefined);
|
||||
assert.equal(provider.getItemDirectory, undefined);
|
||||
});
|
||||
|
||||
test("defaults external at rich text providers to include agent targets", () => {
|
||||
const providers = createTuttiExternalAtRichTextTriggerProviders({
|
||||
bridge: null
|
||||
});
|
||||
|
||||
assert.ok(providers.some((provider) => provider.id === "agent-target"));
|
||||
});
|
||||
|
||||
test("queries multiple external at providers with one bridge call", async () => {
|
||||
const calls: TuttiExternalAtQueryInput[] = [];
|
||||
const results = await queryTuttiExternalAtRichTextTriggerItems({
|
||||
keyword: "a",
|
||||
maxResults: 10,
|
||||
providerIds: ["workspace-app", "agent-session"],
|
||||
bridge: {
|
||||
at: {
|
||||
query(input) {
|
||||
calls.push(input);
|
||||
return [
|
||||
createQueryResult("workspace-app", "apps", "Apps"),
|
||||
createQueryResult("agent-session", "session", "Session"),
|
||||
createQueryResult("file", "README.md", "README.md")
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
{
|
||||
keyword: "a",
|
||||
maxResults: 10,
|
||||
providers: ["workspace-app", "agent-session"]
|
||||
}
|
||||
]);
|
||||
assert.deepEqual(
|
||||
results.map((item) => item.providerId),
|
||||
["workspace-app", "agent-session"]
|
||||
);
|
||||
});
|
||||
|
||||
test("preserves an explicit empty external at provider filter", async () => {
|
||||
const calls: TuttiExternalAtQueryInput[] = [];
|
||||
const results = await queryTuttiExternalAtRichTextTriggerItems({
|
||||
keyword: "a",
|
||||
providerIds: [],
|
||||
bridge: {
|
||||
at: {
|
||||
query(input) {
|
||||
calls.push(input);
|
||||
return [createQueryResult("workspace-app", "apps", "Apps")];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert.deepEqual(calls, [
|
||||
{
|
||||
keyword: "a",
|
||||
providers: []
|
||||
}
|
||||
]);
|
||||
assert.deepEqual(results, []);
|
||||
assert.deepEqual(
|
||||
createTuttiExternalAtRichTextTriggerProviders({
|
||||
bridge: null,
|
||||
providerIds: []
|
||||
}),
|
||||
[]
|
||||
);
|
||||
});
|
||||
|
||||
test("maps query results to the rich text trigger provider shape", () => {
|
||||
const provider = createTuttiExternalAtRichTextTriggerProvider({
|
||||
providerId: "agent-session",
|
||||
bridge: null
|
||||
});
|
||||
const item = createQueryResult("agent-session", "run-1", "Run 1", {
|
||||
subtitle: "created",
|
||||
thumbnailUrl: "https://example.test/run.png"
|
||||
});
|
||||
|
||||
assert.equal(provider.getItemKey(item), "run-1");
|
||||
assert.equal(provider.getItemLabel(item), "Run 1");
|
||||
assert.equal(provider.getItemSubtitle?.(item), "created");
|
||||
assert.equal(provider.getItemIconUrl?.(item), "https://example.test/run.png");
|
||||
assert.deepEqual(provider.toInsertResult(item), item.insert);
|
||||
});
|
||||
|
||||
test("uses mention presentation icons when thumbnailUrl is absent", () => {
|
||||
const provider = createTuttiExternalAtRichTextTriggerProvider({
|
||||
providerId: "agent-session",
|
||||
bridge: null
|
||||
});
|
||||
const item = createQueryResult("agent-session", "run-1", "Run 1", {
|
||||
insert: {
|
||||
kind: "mention",
|
||||
mention: {
|
||||
entityId: "run-1",
|
||||
label: "Run 1",
|
||||
presentation: {
|
||||
iconUrl: "https://example.test/icon.png"
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
provider.getItemIconUrl?.(item),
|
||||
"https://example.test/icon.png"
|
||||
);
|
||||
});
|
||||
|
||||
test("uses exact host resolve when the optional bridge capability exists", async () => {
|
||||
const provider = createTuttiExternalAtRichTextTriggerProvider({
|
||||
providerId: "workspace-app",
|
||||
getBridge: () => ({
|
||||
at: {
|
||||
query: () => [],
|
||||
resolve(input) {
|
||||
assert.deepEqual(input, {
|
||||
providerId: "workspace-app",
|
||||
entityId: "canvas",
|
||||
scope: { workspaceId: "workspace-1" }
|
||||
});
|
||||
return {
|
||||
label: "Canvas",
|
||||
presentation: { iconUrl: "canvas.png" }
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
await provider.resolveMention?.({
|
||||
providerId: "workspace-app",
|
||||
entityId: "canvas",
|
||||
label: "Old canvas",
|
||||
scope: { workspaceId: "workspace-1" }
|
||||
}),
|
||||
{ label: "Canvas", presentation: { iconUrl: "canvas.png" } }
|
||||
);
|
||||
});
|
||||
|
||||
test("falls back to an exact identity and scope match on old hosts", async () => {
|
||||
const calls: TuttiExternalAtQueryInput[] = [];
|
||||
const provider = createTuttiExternalAtRichTextTriggerProvider({
|
||||
providerId: "workspace-app",
|
||||
bridge: {
|
||||
at: {
|
||||
query(input) {
|
||||
calls.push(input);
|
||||
return [
|
||||
createQueryResult("workspace-app", "canvas", "Wrong scope", {
|
||||
insert: {
|
||||
kind: "mention",
|
||||
mention: {
|
||||
entityId: "canvas",
|
||||
label: "Wrong scope",
|
||||
scope: { workspaceId: "workspace-2" }
|
||||
}
|
||||
}
|
||||
}),
|
||||
createQueryResult("workspace-app", "canvas", "Canvas", {
|
||||
insert: {
|
||||
kind: "mention",
|
||||
mention: {
|
||||
entityId: "canvas",
|
||||
label: "Canvas",
|
||||
scope: { workspaceId: "workspace-1" },
|
||||
presentation: { iconUrl: "canvas.png" }
|
||||
}
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const resolved = await provider.resolveMention?.({
|
||||
providerId: "workspace-app",
|
||||
entityId: "canvas",
|
||||
label: "Old canvas",
|
||||
scope: { workspaceId: "workspace-1" }
|
||||
});
|
||||
assert.deepEqual(calls, [
|
||||
{ keyword: "Old canvas", maxResults: 50, providers: ["workspace-app"] }
|
||||
]);
|
||||
assert.deepEqual(resolved, {
|
||||
label: "Canvas",
|
||||
presentation: { iconUrl: "canvas.png" }
|
||||
});
|
||||
});
|
||||
|
||||
test("old host resolution retries with an empty keyword after a label miss", async () => {
|
||||
const calls: TuttiExternalAtQueryInput[] = [];
|
||||
const provider = createTuttiExternalAtRichTextTriggerProvider({
|
||||
providerId: "workspace-app",
|
||||
bridge: {
|
||||
at: {
|
||||
query(input) {
|
||||
calls.push(input);
|
||||
return input.keyword
|
||||
? []
|
||||
: [
|
||||
createQueryResult("workspace-app", "canvas", "Renamed", {
|
||||
insert: {
|
||||
kind: "mention",
|
||||
mention: {
|
||||
entityId: "canvas",
|
||||
label: "Renamed",
|
||||
scope: { workspaceId: "workspace-1" }
|
||||
}
|
||||
}
|
||||
})
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
await provider.resolveMention?.({
|
||||
providerId: "workspace-app",
|
||||
entityId: "canvas",
|
||||
label: "Old canvas",
|
||||
scope: { workspaceId: "workspace-1" }
|
||||
}),
|
||||
{ label: "Renamed", presentation: undefined }
|
||||
);
|
||||
assert.deepEqual(calls, [
|
||||
{ keyword: "Old canvas", maxResults: 50, providers: ["workspace-app"] },
|
||||
{ keyword: "", maxResults: 50, providers: ["workspace-app"] }
|
||||
]);
|
||||
});
|
||||
|
||||
test("external service invalidates from bridge events and unsubscribes", async () => {
|
||||
let listener:
|
||||
| ((event: { providerIds?: readonly ["workspace-app"] }) => void)
|
||||
| undefined;
|
||||
let unsubscribeCalls = 0;
|
||||
let icon = "first.png";
|
||||
const service = createTuttiExternalRichTextMentionService({
|
||||
providerIds: ["workspace-app"],
|
||||
getBridge: () => ({
|
||||
at: {
|
||||
query: () => [],
|
||||
resolve: () => ({ presentation: { iconUrl: icon } }),
|
||||
subscribe(nextListener) {
|
||||
listener = nextListener;
|
||||
return () => {
|
||||
unsubscribeCalls += 1;
|
||||
};
|
||||
}
|
||||
}
|
||||
})
|
||||
});
|
||||
const mention = {
|
||||
providerId: "workspace-app",
|
||||
entityId: "canvas",
|
||||
label: "Canvas"
|
||||
};
|
||||
await service.resolve(mention);
|
||||
icon = "second.png";
|
||||
listener?.({ providerIds: ["workspace-app"] });
|
||||
await service.resolve(mention);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
assert.equal(
|
||||
service.getSnapshot(mention).resolved?.presentation?.iconUrl,
|
||||
"second.png"
|
||||
);
|
||||
service.dispose();
|
||||
service.dispose();
|
||||
assert.equal(unsubscribeCalls, 1);
|
||||
});
|
||||
|
||||
test("external service getBridge remains SSR-safe when unavailable", async () => {
|
||||
const service = createTuttiExternalRichTextMentionService({
|
||||
getBridge: () => undefined,
|
||||
providerIds: ["workspace-app"]
|
||||
});
|
||||
assert.equal(
|
||||
(
|
||||
await service.resolve({
|
||||
providerId: "workspace-app",
|
||||
entityId: "canvas",
|
||||
label: "Canvas"
|
||||
})
|
||||
).state,
|
||||
"missing"
|
||||
);
|
||||
});
|
||||
|
||||
function createQueryResult(
|
||||
providerId: TuttiExternalAtQueryResult["providerId"],
|
||||
itemId: string,
|
||||
label: string,
|
||||
overrides: Partial<TuttiExternalAtQueryResult> = {}
|
||||
): TuttiExternalAtQueryResult {
|
||||
return {
|
||||
providerId,
|
||||
itemId,
|
||||
label,
|
||||
insert: {
|
||||
kind: "mention",
|
||||
mention: {
|
||||
entityId: itemId,
|
||||
label
|
||||
}
|
||||
},
|
||||
...overrides
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
import {
|
||||
tuttiExternalAtProviderIds,
|
||||
type TuttiExternalAtInvalidation,
|
||||
type TuttiExternalAtQueryDirectoryInput,
|
||||
type TuttiExternalAtProviderId,
|
||||
type TuttiExternalAtQueryInput,
|
||||
type TuttiExternalAtQueryResult,
|
||||
type TuttiExternalReferenceSelection,
|
||||
type TuttiExternalAtResolveInput,
|
||||
type TuttiExternalAtResolveResult
|
||||
} from "../contracts/index.ts";
|
||||
import type {
|
||||
RichTextMentionIdentity,
|
||||
RichTextTriggerInsertResult,
|
||||
RichTextTriggerProvider
|
||||
} from "@tutti-os/ui-rich-text/types";
|
||||
import {
|
||||
appendRichTextLinksToContent,
|
||||
createRichTextMentionMarkdown,
|
||||
extractRichTextMentionsFromContent,
|
||||
normalizeRichTextContent
|
||||
} from "@tutti-os/ui-rich-text/core";
|
||||
import {
|
||||
createRichTextMentionService,
|
||||
canonicalizeRichTextMentionScope,
|
||||
type RichTextMentionService
|
||||
} from "@tutti-os/ui-rich-text/service";
|
||||
|
||||
export interface TuttiExternalAtRichTextBridge {
|
||||
at?: {
|
||||
query(
|
||||
input: TuttiExternalAtQueryInput
|
||||
):
|
||||
| Promise<readonly TuttiExternalAtQueryResult[]>
|
||||
| readonly TuttiExternalAtQueryResult[];
|
||||
queryDirectory?(
|
||||
input: TuttiExternalAtQueryDirectoryInput
|
||||
):
|
||||
| Promise<readonly TuttiExternalAtQueryResult[]>
|
||||
| readonly TuttiExternalAtQueryResult[];
|
||||
resolve?(
|
||||
input: TuttiExternalAtResolveInput
|
||||
):
|
||||
| Promise<TuttiExternalAtResolveResult | null>
|
||||
| TuttiExternalAtResolveResult
|
||||
| null;
|
||||
subscribe?(
|
||||
listener: (event: TuttiExternalAtInvalidation) => void
|
||||
): () => void;
|
||||
};
|
||||
}
|
||||
|
||||
export interface CreateTuttiExternalAtRichTextTriggerProviderInput {
|
||||
bridge?: TuttiExternalAtRichTextBridge | null;
|
||||
getBridge?: () => TuttiExternalAtRichTextBridge | null | undefined;
|
||||
providerId: TuttiExternalAtProviderId;
|
||||
maxResults?: number;
|
||||
}
|
||||
|
||||
export interface CreateTuttiExternalAtRichTextTriggerProvidersInput {
|
||||
bridge?: TuttiExternalAtRichTextBridge | null;
|
||||
getBridge?: () => TuttiExternalAtRichTextBridge | null | undefined;
|
||||
providerIds?: readonly TuttiExternalAtProviderId[];
|
||||
maxResults?: number;
|
||||
}
|
||||
|
||||
export interface QueryTuttiExternalAtRichTextTriggerItemsInput {
|
||||
bridge?: TuttiExternalAtRichTextBridge | null;
|
||||
getBridge?: () => TuttiExternalAtRichTextBridge | null | undefined;
|
||||
keyword: string;
|
||||
providerIds?: readonly TuttiExternalAtProviderId[];
|
||||
maxResults?: number;
|
||||
}
|
||||
|
||||
export interface CreateTuttiExternalRichTextMentionServiceInput {
|
||||
getBridge: () => TuttiExternalAtRichTextBridge | null | undefined;
|
||||
providerIds?: readonly TuttiExternalAtProviderId[];
|
||||
appLocalProviders?: readonly RichTextTriggerProvider[];
|
||||
maxResults?: number;
|
||||
}
|
||||
|
||||
function workspaceReferenceIdentityKey(input: {
|
||||
entityId: string;
|
||||
providerId: string;
|
||||
scope?: Readonly<Record<string, string>>;
|
||||
}): string | null {
|
||||
if (input.providerId !== "workspace-reference") {
|
||||
return null;
|
||||
}
|
||||
const entityId = input.entityId.trim();
|
||||
const source = input.scope?.source?.trim() ?? "";
|
||||
const workspaceId = input.scope?.workspaceId?.trim() ?? "";
|
||||
if (!entityId || source !== "app" || !workspaceId) {
|
||||
return null;
|
||||
}
|
||||
return JSON.stringify([
|
||||
entityId,
|
||||
source,
|
||||
workspaceId,
|
||||
input.scope?.groupId?.trim() ?? ""
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends Host-selected paths and lazy workspace-reference handles to the
|
||||
* serialized rich-text prompt. Selection order is stable within each kind,
|
||||
* duplicate paths or handles are omitted, and existing prompt text is kept.
|
||||
*/
|
||||
export function appendTuttiExternalReferenceSelections(
|
||||
value: string | null | undefined,
|
||||
selections: readonly TuttiExternalReferenceSelection[]
|
||||
): string {
|
||||
const pathSelections = selections.filter(
|
||||
(
|
||||
selection
|
||||
): selection is Extract<
|
||||
TuttiExternalReferenceSelection,
|
||||
{ selectionKind: "path" }
|
||||
> => selection.selectionKind === "path"
|
||||
);
|
||||
let content = appendRichTextLinksToContent(
|
||||
value,
|
||||
pathSelections.map(({ reference }) => ({
|
||||
kind: reference.kind === "folder" ? "folder" : "file",
|
||||
name: reference.displayName,
|
||||
path: reference.path
|
||||
}))
|
||||
);
|
||||
const existingWorkspaceReferenceKeys = new Set(
|
||||
extractRichTextMentionsFromContent(content).flatMap((mention) => {
|
||||
const key = workspaceReferenceIdentityKey(mention);
|
||||
return key ? [key] : [];
|
||||
})
|
||||
);
|
||||
const mentionMarkdown = selections.flatMap((selection) => {
|
||||
if (selection.selectionKind !== "workspace-reference") {
|
||||
return [];
|
||||
}
|
||||
const mention = {
|
||||
providerId: "workspace-reference",
|
||||
entityId: selection.id,
|
||||
label: selection.displayName,
|
||||
scope: {
|
||||
workspaceId: selection.workspaceId,
|
||||
source: selection.source,
|
||||
...(selection.groupId ? { groupId: selection.groupId } : {}),
|
||||
...(selection.fileCount && selection.fileCount > 0
|
||||
? { count: String(selection.fileCount) }
|
||||
: {})
|
||||
}
|
||||
};
|
||||
const key = workspaceReferenceIdentityKey(mention);
|
||||
if (!key || existingWorkspaceReferenceKeys.has(key)) {
|
||||
return [];
|
||||
}
|
||||
existingWorkspaceReferenceKeys.add(key);
|
||||
const markdown = createRichTextMentionMarkdown(mention);
|
||||
return markdown ? [markdown] : [];
|
||||
});
|
||||
if (mentionMarkdown.length === 0) {
|
||||
return content;
|
||||
}
|
||||
content = normalizeRichTextContent(content);
|
||||
return content
|
||||
? `${content} ${mentionMarkdown.join(" ")}`
|
||||
: mentionMarkdown.join(" ");
|
||||
}
|
||||
|
||||
export async function queryTuttiExternalAtRichTextTriggerItems(
|
||||
input: QueryTuttiExternalAtRichTextTriggerItemsInput
|
||||
): Promise<readonly TuttiExternalAtQueryResult[]> {
|
||||
const bridge = (input.getBridge?.() ?? input.bridge)?.at;
|
||||
if (!bridge) return [];
|
||||
|
||||
const providerIds =
|
||||
input.providerIds === undefined
|
||||
? tuttiExternalAtProviderIds
|
||||
: input.providerIds;
|
||||
const results = await bridge.query({
|
||||
keyword: input.keyword,
|
||||
...(input.maxResults !== undefined ? { maxResults: input.maxResults } : {}),
|
||||
providers: providerIds
|
||||
});
|
||||
const providerSet = new Set<TuttiExternalAtProviderId>(providerIds);
|
||||
return results.filter((item) => providerSet.has(item.providerId));
|
||||
}
|
||||
|
||||
export function createTuttiExternalAtRichTextTriggerProvider(
|
||||
input: CreateTuttiExternalAtRichTextTriggerProviderInput
|
||||
): RichTextTriggerProvider<TuttiExternalAtQueryResult> {
|
||||
return {
|
||||
id: input.providerId,
|
||||
trigger: "@",
|
||||
async query(queryInput) {
|
||||
return queryTuttiExternalAtRichTextTriggerItems({
|
||||
bridge: input.bridge,
|
||||
getBridge: input.getBridge,
|
||||
keyword: queryInput.keyword,
|
||||
maxResults: queryInput.maxResults ?? input.maxResults,
|
||||
providerIds: [input.providerId]
|
||||
});
|
||||
},
|
||||
...(input.providerId === "file" &&
|
||||
(input.getBridge?.() ?? input.bridge)?.at?.queryDirectory
|
||||
? {
|
||||
getItemDirectory(item: TuttiExternalAtQueryResult) {
|
||||
return item.directory;
|
||||
},
|
||||
async queryDirectory(queryInput) {
|
||||
const bridge = (input.getBridge?.() ?? input.bridge)?.at;
|
||||
if (!bridge?.queryDirectory) {
|
||||
throw new Error(
|
||||
"Tutti external @ bridge does not support directory browsing."
|
||||
);
|
||||
}
|
||||
return bridge.queryDirectory({
|
||||
directoryPath: queryInput.directoryPath,
|
||||
maxResults: queryInput.maxResults ?? input.maxResults,
|
||||
providerId: input.providerId
|
||||
});
|
||||
}
|
||||
}
|
||||
: {}),
|
||||
async resolveMention(identity) {
|
||||
const bridge = (input.getBridge?.() ?? input.bridge)?.at;
|
||||
if (!bridge) return null;
|
||||
if (bridge.resolve) {
|
||||
return bridge.resolve({
|
||||
providerId: input.providerId,
|
||||
entityId: identity.entityId,
|
||||
...(identity.scope ? { scope: identity.scope } : {})
|
||||
});
|
||||
}
|
||||
const fallbackKeywords = [
|
||||
identity.label.trim().replace(/^@+/, "").trim(),
|
||||
""
|
||||
].filter((keyword, index, values) => values.indexOf(keyword) === index);
|
||||
for (const keyword of fallbackKeywords) {
|
||||
const matches = await queryTuttiExternalAtRichTextTriggerItems({
|
||||
bridge: input.bridge,
|
||||
getBridge: input.getBridge,
|
||||
keyword,
|
||||
maxResults: 50,
|
||||
providerIds: [input.providerId]
|
||||
});
|
||||
const match = matches.find((item) =>
|
||||
matchesExternalMentionIdentity(item, identity)
|
||||
);
|
||||
if (match?.insert.kind === "mention") {
|
||||
return {
|
||||
label: match.insert.mention.label,
|
||||
presentation: match.insert.mention.presentation
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
getItemKey: (item) => item.itemId,
|
||||
getItemLabel: (item) => item.label,
|
||||
getItemSubtitle: (item) => item.subtitle,
|
||||
getItemIconUrl: (item) =>
|
||||
item.thumbnailUrl ??
|
||||
(item.insert.kind === "mention"
|
||||
? (item.insert.mention.presentation?.iconUrl ??
|
||||
item.insert.mention.presentation?.thumbnailUrl ??
|
||||
item.insert.mention.presentation?.agentIconUrl)
|
||||
: undefined),
|
||||
toInsertResult: (item) => item.insert as RichTextTriggerInsertResult
|
||||
};
|
||||
}
|
||||
|
||||
export function createTuttiExternalAtRichTextTriggerProviders(
|
||||
input: CreateTuttiExternalAtRichTextTriggerProvidersInput
|
||||
): readonly RichTextTriggerProvider<TuttiExternalAtQueryResult>[] {
|
||||
const providerIds =
|
||||
input.providerIds === undefined
|
||||
? tuttiExternalAtProviderIds
|
||||
: input.providerIds;
|
||||
return providerIds.map((providerId) =>
|
||||
createTuttiExternalAtRichTextTriggerProvider({
|
||||
bridge: input.bridge,
|
||||
getBridge: input.getBridge,
|
||||
providerId,
|
||||
maxResults: input.maxResults
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export function createTuttiExternalRichTextMentionService(
|
||||
input: CreateTuttiExternalRichTextMentionServiceInput
|
||||
): RichTextMentionService {
|
||||
const hostProviders = createTuttiExternalAtRichTextTriggerProviders({
|
||||
getBridge: input.getBridge,
|
||||
providerIds: input.providerIds,
|
||||
maxResults: input.maxResults
|
||||
});
|
||||
const service = createRichTextMentionService({
|
||||
providers: [...hostProviders, ...(input.appLocalProviders ?? [])]
|
||||
});
|
||||
const unsubscribe = input.getBridge()?.at?.subscribe?.((event) => {
|
||||
invalidateFromExternalEvent(service, event);
|
||||
});
|
||||
const disposeService = service.dispose.bind(service);
|
||||
let disposed = false;
|
||||
service.dispose = () => {
|
||||
if (disposed) return;
|
||||
disposed = true;
|
||||
unsubscribe?.();
|
||||
disposeService();
|
||||
};
|
||||
return service;
|
||||
}
|
||||
|
||||
function matchesExternalMentionIdentity(
|
||||
item: TuttiExternalAtQueryResult,
|
||||
identity: RichTextMentionIdentity
|
||||
): boolean {
|
||||
if (
|
||||
item.providerId !== identity.providerId ||
|
||||
item.insert.kind !== "mention"
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
item.insert.mention.entityId.trim() === identity.entityId.trim() &&
|
||||
canonicalizeRichTextMentionScope(item.insert.mention.scope) ===
|
||||
canonicalizeRichTextMentionScope(identity.scope)
|
||||
);
|
||||
}
|
||||
|
||||
function invalidateFromExternalEvent(
|
||||
service: RichTextMentionService,
|
||||
event: TuttiExternalAtInvalidation
|
||||
): void {
|
||||
const providerIds = event.providerIds?.length
|
||||
? event.providerIds
|
||||
: [undefined];
|
||||
const entityIds = event.entityIds?.length ? event.entityIds : [undefined];
|
||||
for (const providerId of providerIds) {
|
||||
for (const entityId of entityIds) {
|
||||
service.invalidate({ providerId, entityId });
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user