feat(core): add ai workspace all docs switch (#13345)

Close [AI-397](https://linear.app/affine-design/issue/AI-397)

<img width="272" height="186" alt="截屏2025-07-29 11 54 20"
src="https://github.com/user-attachments/assets/e171fb57-66cf-4244-894d-c27b18cbe83a"
/>


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Introduced an AI tools configuration service, allowing users to
customize AI tool usage (e.g., workspace search, reading docs) in chat
and AI features.
* Added a toggle in chat preferences for enabling or disabling
workspace-wide document search.
* AI chat components now respect user-configured tool settings across
chat, retry, and playground scenarios.

* **Improvements**
* Enhanced chat and AI interfaces to propagate and honor user tool
configuration throughout the frontend and backend.
* Made draft and tool configuration services optional and safely handled
their absence in chat components.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Wu Yue
2025-07-30 10:10:39 +08:00
committed by GitHub
parent 091bac1047
commit f7a094053e
23 changed files with 321 additions and 61 deletions
@@ -1,3 +1,4 @@
import type { AIToolsConfig } from '@affine/core/modules/ai-button';
import type {
AddContextFileInput,
ContextMatchedDocChunk,
@@ -142,6 +143,7 @@ declare global {
webSearch?: boolean;
reasoning?: boolean;
modelId?: string;
toolsConfig?: AIToolsConfig | undefined;
contexts?: {
docs: AIDocContextOption[];
files: AIFileContextOption[];
@@ -1,3 +1,4 @@
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { AppThemeService } from '@affine/core/modules/theme';
@@ -105,6 +106,9 @@ export class AIChatPanelTitle extends SignalWatcher(
@property({ attribute: false })
accessor notificationService!: NotificationService;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@property({ attribute: false })
accessor session!: CopilotChatHistoryFragment | null | undefined;
@@ -142,6 +146,7 @@ export class AIChatPanelTitle extends SignalWatcher(
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
.aiToolsConfigService=${this.aiToolsConfigService}
></playground-content>
`;
@@ -1,4 +1,7 @@
import type { AIDraftService } from '@affine/core/modules/ai-button';
import type {
AIDraftService,
AIToolsConfigService,
} from '@affine/core/modules/ai-button';
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { AppThemeService } from '@affine/core/modules/theme';
@@ -119,6 +122,9 @@ export class ChatPanel extends SignalWatcher(
@property({ attribute: false })
accessor aiDraftService!: AIDraftService;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@state()
accessor session: CopilotChatHistoryFragment | null | undefined;
@@ -387,6 +393,7 @@ export class ChatPanel extends SignalWatcher(
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
.aiToolsConfigService=${this.aiToolsConfigService}
.session=${this.session}
.status=${this.status}
.embeddingProgress=${this.embeddingProgress}
@@ -413,6 +420,7 @@ export class ChatPanel extends SignalWatcher(
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
.aiDraftService=${this.aiDraftService}
.aiToolsConfigService=${this.aiToolsConfigService}
.onEmbeddingProgressChange=${this.onEmbeddingProgressChange}
.onContextChange=${this.onContextChange}
.width=${this.sidebarWidth}
@@ -1,6 +1,9 @@
import './ai-chat-composer-tip';
import type { AIDraftService } from '@affine/core/modules/ai-button';
import type {
AIDraftService,
AIToolsConfigService,
} from '@affine/core/modules/ai-button';
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type {
ContextEmbedStatus,
@@ -118,7 +121,10 @@ export class AIChatComposer extends SignalWatcher(
accessor notificationService!: NotificationService;
@property({ attribute: false })
accessor aiDraftService!: AIDraftService;
accessor aiDraftService: AIDraftService | undefined;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@state()
accessor chips: ChatChip[] = [];
@@ -166,6 +172,7 @@ export class AIChatComposer extends SignalWatcher(
.docDisplayConfig=${this.docDisplayConfig}
.searchMenuConfig=${this.searchMenuConfig}
.aiDraftService=${this.aiDraftService}
.aiToolsConfigService=${this.aiToolsConfigService}
.portalContainer=${this.portalContainer}
.onChatSuccess=${this.onChatSuccess}
.trackOptions=${this.trackOptions}
@@ -1,4 +1,7 @@
import type { AIDraftService } from '@affine/core/modules/ai-button';
import type {
AIDraftService,
AIToolsConfigService,
} from '@affine/core/modules/ai-button';
import type { AIDraftState } from '@affine/core/modules/ai-button/services/ai-draft';
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
@@ -153,7 +156,10 @@ export class AIChatContent extends SignalWatcher(
accessor notificationService!: NotificationService;
@property({ attribute: false })
accessor aiDraftService!: AIDraftService;
accessor aiDraftService: AIDraftService | undefined;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@property({ attribute: false })
accessor onEmbeddingProgressChange:
@@ -273,6 +279,9 @@ export class AIChatContent extends SignalWatcher(
};
private readonly updateDraft = async (context: Partial<ChatContextValue>) => {
if (!this.aiDraftService) {
return;
}
const draft: Partial<AIDraftState> = pick(context, [
'quote',
'images',
@@ -344,15 +353,17 @@ export class AIChatContent extends SignalWatcher(
this.initChatContent().catch(console.error);
this.aiDraftService
.getDraft()
.then(draft => {
this.chatContextValue = {
...this.chatContextValue,
...draft,
};
})
.catch(console.error);
if (this.aiDraftService) {
this.aiDraftService
.getDraft()
.then(draft => {
this.chatContextValue = {
...this.chatContextValue,
...draft,
};
})
.catch(console.error);
}
this._disposables.add(
AIProvider.slots.actions.subscribe(({ event }) => {
@@ -405,6 +416,7 @@ export class AIChatContent extends SignalWatcher(
.affineFeatureFlagService=${this.affineFeatureFlagService}
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
.aiToolsConfigService=${this.aiToolsConfigService}
.networkSearchConfig=${this.networkSearchConfig}
.reasoningConfig=${this.reasoningConfig}
.width=${this.width}
@@ -434,6 +446,7 @@ export class AIChatContent extends SignalWatcher(
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
.notificationService=${this.notificationService}
.aiDraftService=${this.aiDraftService}
.aiToolsConfigService=${this.aiToolsConfigService}
.trackOptions=${{
where: 'chat-panel',
control: 'chat-send',
@@ -1,4 +1,7 @@
import type { AIDraftService } from '@affine/core/modules/ai-button';
import type {
AIDraftService,
AIToolsConfigService,
} from '@affine/core/modules/ai-button';
import type { CopilotChatHistoryFragment } from '@affine/graphql';
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
import { unsafeCSSVar, unsafeCSSVarV2 } from '@blocksuite/affine/shared/theme';
@@ -353,7 +356,10 @@ export class AIChatInput extends SignalWatcher(
accessor searchMenuConfig!: SearchMenuConfig;
@property({ attribute: false })
accessor aiDraftService!: AIDraftService;
accessor aiDraftService: AIDraftService | undefined;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@property({ attribute: false })
accessor isRootSession: boolean = true;
@@ -406,13 +412,15 @@ export class AIChatInput extends SignalWatcher(
protected override firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties);
this.aiDraftService
.getDraft()
.then(draft => {
this.textarea.value = draft.input;
this.isInputEmpty = !this.textarea.value.trim();
})
.catch(console.error);
if (this.aiDraftService) {
this.aiDraftService
.getDraft()
.then(draft => {
this.textarea.value = draft.input;
this.isInputEmpty = !this.textarea.value.trim();
})
.catch(console.error);
}
}
protected override render() {
@@ -493,6 +501,7 @@ export class AIChatInput extends SignalWatcher(
.networkSearchVisible=${!!this.networkSearchConfig.visible.value}
.isNetworkActive=${this._isNetworkActive}
.onNetworkActiveChange=${this._toggleNetworkSearch}
.toolsConfigService=${this.aiToolsConfigService}
></chat-input-preference>
${status === 'transmitting' || status === 'loading'
? html`<button
@@ -536,9 +545,11 @@ export class AIChatInput extends SignalWatcher(
textarea.style.overflowY = 'scroll';
}
await this.aiDraftService.setDraft({
input: value,
});
if (this.aiDraftService) {
await this.aiDraftService.setDraft({
input: value,
});
}
};
private readonly _handleKeyDown = async (evt: KeyboardEvent) => {
@@ -593,9 +604,11 @@ export class AIChatInput extends SignalWatcher(
this.isInputEmpty = true;
this.textarea.style.height = 'unset';
await this.aiDraftService.setDraft({
input: '',
});
if (this.aiDraftService) {
await this.aiDraftService.setDraft({
input: '',
});
}
await this.send(value);
};
@@ -647,6 +660,7 @@ export class AIChatInput extends SignalWatcher(
control: this.trackOptions?.control,
webSearch: this._isNetworkActive,
reasoning: this._isReasoningActive,
toolsConfig: this.aiToolsConfigService.config.value,
modelId: this.modelId,
});
@@ -1,3 +1,4 @@
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
import type { CopilotChatHistoryFragment } from '@affine/graphql';
import {
menu,
@@ -7,6 +8,7 @@ import {
import { SignalWatcher, WithDisposable } from '@blocksuite/affine/global/lit';
import {
ArrowDownSmallIcon,
CloudWorkspaceIcon,
ThinkingIcon,
WebIcon,
} from '@blocksuite/icons/lit';
@@ -81,6 +83,9 @@ export class ChatInputPreference extends SignalWatcher(
| undefined;
// --------- search props end ---------
@property({ attribute: false })
accessor toolsConfigService!: AIToolsConfigService;
// private readonly _onModelChange = (modelId: string) => {
// this.onModelChange?.(modelId);
// };
@@ -126,6 +131,19 @@ export class ChatInputPreference extends SignalWatcher(
onChange: (value: boolean) => this.onNetworkActiveChange?.(value),
class: { 'preference-action': true },
testId: 'chat-network-search',
}),
menu.toggleSwitch({
name: 'Workspace All Docs',
prefix: CloudWorkspaceIcon(),
on:
!!this.toolsConfigService.config.value.searchWorkspace &&
!!this.toolsConfigService.config.value.readingDocs,
onChange: (value: boolean) =>
this.toolsConfigService.setConfig({
searchWorkspace: value,
readingDocs: value,
}),
class: { 'preference-action': true },
})
);
}
@@ -1,3 +1,4 @@
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
import type { AppThemeService } from '@affine/core/modules/theme';
import type { CopilotChatHistoryFragment } from '@affine/graphql';
import { WithDisposable } from '@blocksuite/affine/global/lit';
@@ -206,6 +207,9 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
@property({ attribute: false })
accessor docDisplayService!: DocDisplayConfig;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@property({ attribute: false })
accessor onOpenDoc!: (docId: string, sessionId?: string) => void;
@@ -467,6 +471,7 @@ export class AIChatMessages extends WithDisposable(ShadowlessElement) {
isRootSession: true,
reasoning: this._isReasoningActive,
webSearch: this._isNetworkActive,
toolsConfig: this.aiToolsConfigService.config.value,
});
for await (const text of stream) {
@@ -1,3 +1,4 @@
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { AppThemeService } from '@affine/core/modules/theme';
@@ -173,6 +174,9 @@ export class PlaygroundChat extends SignalWatcher(
@property({ attribute: false })
accessor notificationService!: NotificationService;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@property({ attribute: false })
accessor addChat!: () => Promise<void>;
@@ -338,6 +342,7 @@ export class PlaygroundChat extends SignalWatcher(
.affineFeatureFlagService=${this.affineFeatureFlagService}
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
.aiToolsConfigService=${this.aiToolsConfigService}
.networkSearchConfig=${this.networkSearchConfig}
.reasoningConfig=${this.reasoningConfig}
.messages=${this.messages}
@@ -357,6 +362,7 @@ export class PlaygroundChat extends SignalWatcher(
.docDisplayConfig=${this.docDisplayConfig}
.searchMenuConfig=${this.searchMenuConfig}
.notificationService=${this.notificationService}
.aiToolsConfigService=${this.aiToolsConfigService}
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
></ai-chat-composer>
</div>`;
@@ -1,3 +1,4 @@
import type { AIToolsConfigService } from '@affine/core/modules/ai-button';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type { AppThemeService } from '@affine/core/modules/theme';
import type { CopilotChatHistoryFragment } from '@affine/graphql';
@@ -92,6 +93,9 @@ export class PlaygroundContent extends SignalWatcher(
@property({ attribute: false })
accessor notificationService!: NotificationService;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@state()
accessor sessions: CopilotChatHistoryFragment[] = [];
@@ -347,6 +351,7 @@ export class PlaygroundContent extends SignalWatcher(
.affineFeatureFlagService=${this.affineFeatureFlagService}
.affineThemeService=${this.affineThemeService}
.notificationService=${this.notificationService}
.aiToolsConfigService=${this.aiToolsConfigService}
.addChat=${this.addChat}
></playground-chat>
</div>
@@ -1,3 +1,7 @@
import type {
AIDraftService,
AIToolsConfigService,
} from '@affine/core/modules/ai-button';
import type { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import type { FeatureFlagService } from '@affine/core/modules/feature-flag';
import type {
@@ -393,6 +397,7 @@ export class AIChatBlockPeekView extends LitElement {
control: 'chat-send',
reasoning: this._isReasoningActive,
webSearch: this._isNetworkActive,
toolsConfig: this.aiToolsConfigService.config.value,
});
for await (const text of stream) {
@@ -608,6 +613,7 @@ export class AIChatBlockPeekView extends LitElement {
.searchMenuConfig=${this.searchMenuConfig}
.affineWorkspaceDialogService=${this.affineWorkspaceDialogService}
.notificationService=${notificationService}
.aiToolsConfigService=${this.aiToolsConfigService}
.onChatSuccess=${this._onChatSuccess}
.trackOptions=${{
where: 'ai-chat-block',
@@ -646,6 +652,12 @@ export class AIChatBlockPeekView extends LitElement {
@property({ attribute: false })
accessor affineWorkspaceDialogService!: WorkspaceDialogService;
@property({ attribute: false })
accessor aiDraftService!: AIDraftService;
@property({ attribute: false })
accessor aiToolsConfigService!: AIToolsConfigService;
@state()
accessor _historyMessages: ChatMessage[] = [];
@@ -682,7 +694,9 @@ export const AIChatBlockPeekViewTemplate = (
networkSearchConfig: AINetworkSearchConfig,
reasoningConfig: AIReasoningConfig,
affineFeatureFlagService: FeatureFlagService,
affineWorkspaceDialogService: WorkspaceDialogService
affineWorkspaceDialogService: WorkspaceDialogService,
aiDraftService: AIDraftService,
aiToolsConfigService: AIToolsConfigService
) => {
return html`<ai-chat-block-peek-view
.blockModel=${blockModel}
@@ -693,5 +707,7 @@ export const AIChatBlockPeekViewTemplate = (
.reasoningConfig=${reasoningConfig}
.affineFeatureFlagService=${affineFeatureFlagService}
.affineWorkspaceDialogService=${affineWorkspaceDialogService}
.aiDraftService=${aiDraftService}
.aiToolsConfigService=${aiToolsConfigService}
></ai-chat-block-peek-view>`;
};
@@ -1,4 +1,5 @@
import { showAILoginRequiredAtom } from '@affine/core/components/affine/auth/ai-login-required';
import type { AIToolsConfig } from '@affine/core/modules/ai-button';
import type { UserFriendlyError } from '@affine/error';
import {
addContextCategoryMutation,
@@ -415,6 +416,7 @@ export class CopilotClient {
reasoning,
webSearch,
modelId,
toolsConfig,
signal,
}: {
sessionId: string;
@@ -422,6 +424,7 @@ export class CopilotClient {
reasoning?: boolean;
webSearch?: boolean;
modelId?: string;
toolsConfig?: AIToolsConfig;
signal?: AbortSignal;
}) {
let url = `/api/copilot/chat/${sessionId}`;
@@ -430,6 +433,7 @@ export class CopilotClient {
reasoning,
webSearch,
modelId,
toolsConfig,
});
if (queryString) {
url += `?${queryString}`;
@@ -446,12 +450,14 @@ export class CopilotClient {
reasoning,
webSearch,
modelId,
toolsConfig,
}: {
sessionId: string;
messageId?: string;
reasoning?: boolean;
webSearch?: boolean;
modelId?: string;
toolsConfig?: AIToolsConfig;
},
endpoint = Endpoint.Stream
) {
@@ -461,6 +467,7 @@ export class CopilotClient {
reasoning,
webSearch,
modelId,
toolsConfig,
});
if (queryString) {
url += `?${queryString}`;
@@ -486,7 +493,9 @@ export class CopilotClient {
return this.eventSource(url);
}
paramsToQueryString(params: Record<string, string | boolean | undefined>) {
paramsToQueryString(
params: Record<string, string | boolean | undefined | Record<string, any>>
) {
const queryString = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
if (typeof value === 'boolean') {
@@ -495,6 +504,8 @@ export class CopilotClient {
}
} else if (typeof value === 'string') {
queryString.append(key, value);
} else if (typeof value === 'object' && value !== null) {
queryString.append(key, JSON.stringify(value));
}
});
return queryString.toString();
@@ -1,3 +1,4 @@
import type { AIToolsConfig } from '@affine/core/modules/ai-button';
import { partition } from 'lodash-es';
import { AIProvider } from './ai-provider';
@@ -22,6 +23,7 @@ export type TextToTextOptions = {
reasoning?: boolean;
webSearch?: boolean;
modelId?: string;
toolsConfig?: AIToolsConfig;
};
export type ToImageOptions = TextToTextOptions & {
@@ -119,6 +121,7 @@ export function textToText({
reasoning,
webSearch,
modelId,
toolsConfig,
}: TextToTextOptions) {
let messageId: string | undefined;
@@ -141,6 +144,7 @@ export function textToText({
reasoning,
webSearch,
modelId,
toolsConfig,
},
endpoint
);