diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/stream-objects.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/stream-objects.ts
index 3ee1a92bf..a873fa85c 100644
--- a/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/stream-objects.ts
+++ b/packages/frontend/core/src/blocksuite/ai/components/ai-message-content/stream-objects.ts
@@ -101,6 +101,21 @@ export class ChatContentStreamObjects extends WithDisposable(
.notificationService=${this.notificationService}
>
`;
+ case 'doc_semantic_search':
+ return html``;
+ case 'doc_keyword_search':
+ return html``;
+ case 'doc_read':
+ return html``;
default: {
const name = streamObject.toolName + ' tool calling';
return html`
@@ -159,6 +174,21 @@ export class ChatContentStreamObjects extends WithDisposable(
.notificationService=${this.notificationService}
>
`;
+ case 'doc_semantic_search':
+ return html``;
+ case 'doc_keyword_search':
+ return html``;
+ case 'doc_read':
+ return html``;
default: {
const name = streamObject.toolName + ' tool result';
return html`
diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-tools/doc-keyword-search-result.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/doc-keyword-search-result.ts
new file mode 100644
index 000000000..834bfb2d5
--- /dev/null
+++ b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/doc-keyword-search-result.ts
@@ -0,0 +1,70 @@
+import { WithDisposable } from '@blocksuite/global/lit';
+import { PageIcon, SearchIcon } from '@blocksuite/icons/lit';
+import { ShadowlessElement } from '@blocksuite/std';
+import type { Signal } from '@preact/signals-core';
+import { html, nothing } from 'lit';
+import { property } from 'lit/decorators.js';
+
+import type { ToolResult } from './tool-result-card';
+
+interface DocKeywordSearchToolCall {
+ type: 'tool-call';
+ toolCallId: string;
+ toolName: string;
+ args: { query: string };
+}
+
+interface DocKeywordSearchToolResult {
+ type: 'tool-result';
+ toolCallId: string;
+ toolName: string;
+ args: { query: string };
+ result: Array<{
+ title: string;
+ docId: string;
+ }>;
+}
+
+export class DocKeywordSearchResult extends WithDisposable(ShadowlessElement) {
+ @property({ attribute: false })
+ accessor data!: DocKeywordSearchToolCall | DocKeywordSearchToolResult;
+
+ @property({ attribute: false })
+ accessor width: Signal | undefined;
+
+ renderToolCall() {
+ return html``;
+ }
+
+ renderToolResult() {
+ if (this.data.type !== 'tool-result') {
+ return nothing;
+ }
+ let results: ToolResult[] = [];
+ try {
+ results = this.data.result.map(item => ({
+ title: item.title,
+ icon: PageIcon(),
+ }));
+ } catch (err) {
+ console.error('Failed to parse result', err);
+ }
+ return html``;
+ }
+
+ protected override render() {
+ if (this.data.type === 'tool-call') {
+ return this.renderToolCall();
+ }
+ return this.renderToolResult();
+ }
+}
diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-tools/doc-read-result.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/doc-read-result.ts
new file mode 100644
index 000000000..09834d006
--- /dev/null
+++ b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/doc-read-result.ts
@@ -0,0 +1,70 @@
+import { WithDisposable } from '@blocksuite/global/lit';
+import { PageIcon, ViewIcon } from '@blocksuite/icons/lit';
+import { ShadowlessElement } from '@blocksuite/std';
+import type { Signal } from '@preact/signals-core';
+import { html, nothing } from 'lit';
+import { property } from 'lit/decorators.js';
+
+interface DocReadToolCall {
+ type: 'tool-call';
+ toolCallId: string;
+ toolName: string;
+ args: { doc_id: string };
+}
+
+interface DocReadToolResult {
+ type: 'tool-result';
+ toolCallId: string;
+ toolName: string;
+ args: { doc_id: string };
+ result: {
+ title: string;
+ markdown: string;
+ };
+}
+
+export class DocReadResult extends WithDisposable(ShadowlessElement) {
+ @property({ attribute: false })
+ accessor data!: DocReadToolCall | DocReadToolResult;
+
+ @property({ attribute: false })
+ accessor width: Signal | undefined;
+
+ renderToolCall() {
+ // TODO: get document name by doc_id
+ return html``;
+ }
+
+ renderToolResult() {
+ if (this.data.type !== 'tool-result') {
+ return nothing;
+ }
+ // TODO: better markdown rendering
+ return html``;
+ }
+
+ protected override render() {
+ if (this.data.type === 'tool-call') {
+ return this.renderToolCall();
+ }
+ if (this.data.type === 'tool-result') {
+ return this.renderToolResult();
+ }
+ return nothing;
+ }
+}
diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-tools/doc-semantic-search-result.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/doc-semantic-search-result.ts
new file mode 100644
index 000000000..c19a8fbaf
--- /dev/null
+++ b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/doc-semantic-search-result.ts
@@ -0,0 +1,100 @@
+import { WithDisposable } from '@blocksuite/global/lit';
+import { AiEmbeddingIcon, PageIcon } from '@blocksuite/icons/lit';
+import { ShadowlessElement } from '@blocksuite/std';
+import type { Signal } from '@preact/signals-core';
+import { html, nothing } from 'lit';
+import { property } from 'lit/decorators.js';
+
+interface DocSemanticSearchToolCall {
+ type: 'tool-call';
+ toolCallId: string;
+ toolName: string;
+ args: { query: string };
+}
+
+interface DocSemanticSearchToolResult {
+ type: 'tool-result';
+ toolCallId: string;
+ toolName: string;
+ args: { query: string };
+ result: Array<{
+ content: string;
+ docId: string;
+ }>;
+}
+
+function parseResultContent(content: string) {
+ const properties = [
+ 'Title',
+ 'Created at',
+ 'Updated at',
+ 'Created by',
+ 'Updated by',
+ ];
+ try {
+ // A row starts with "Title: ${title}\n"
+ const title = content.match(/^Title:\s+(.*)\n/)?.[1];
+ // from first row that not starts with "${propertyName}:" to end of the content
+ const rows = content.split('\n');
+ const startIndex = rows.findIndex(
+ line => !properties.some(property => line.startsWith(`${property}:`))
+ );
+ const text = rows.slice(startIndex).join('\n');
+ return {
+ title,
+ content: text,
+ icon: PageIcon(),
+ };
+ } catch (error) {
+ console.error('Failed to parse result content', error);
+ return null;
+ }
+}
+
+export class DocSemanticSearchResult extends WithDisposable(ShadowlessElement) {
+ @property({ attribute: false })
+ accessor data!: DocSemanticSearchToolCall | DocSemanticSearchToolResult;
+
+ @property({ attribute: false })
+ accessor width: Signal | undefined;
+
+ renderToolCall() {
+ return html``;
+ }
+
+ renderToolResult() {
+ if (this.data.type !== 'tool-result') {
+ return nothing;
+ }
+ return html` parseResultContent(result.content))
+ .filter(Boolean)}
+ >`;
+ }
+
+ protected override render() {
+ const { data } = this;
+
+ if (data.type === 'tool-call') {
+ return this.renderToolCall();
+ }
+ if (data.type === 'tool-result') {
+ return this.renderToolResult();
+ }
+ return nothing;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'doc-semantic-search-result': DocSemanticSearchResult;
+ }
+}
diff --git a/packages/frontend/core/src/blocksuite/ai/components/ai-tools/tool-result-card.ts b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/tool-result-card.ts
index ff71d4e77..5264b2cc6 100644
--- a/packages/frontend/core/src/blocksuite/ai/components/ai-tools/tool-result-card.ts
+++ b/packages/frontend/core/src/blocksuite/ai/components/ai-tools/tool-result-card.ts
@@ -7,7 +7,7 @@ import { type Signal } from '@preact/signals-core';
import { css, html, nothing, type TemplateResult } from 'lit';
import { property, state } from 'lit/decorators.js';
-interface ToolResult {
+export interface ToolResult {
title: string;
icon?: string | TemplateResult<1>;
content?: string;
diff --git a/packages/frontend/core/src/blocksuite/ai/effects.ts b/packages/frontend/core/src/blocksuite/ai/effects.ts
index 3728c19ec..60fcbaf7a 100644
--- a/packages/frontend/core/src/blocksuite/ai/effects.ts
+++ b/packages/frontend/core/src/blocksuite/ai/effects.ts
@@ -57,6 +57,9 @@ import {
} from './components/ai-tools/code-artifact';
import { DocComposeTool } from './components/ai-tools/doc-compose';
import { DocEditTool } from './components/ai-tools/doc-edit';
+import { DocKeywordSearchResult } from './components/ai-tools/doc-keyword-search-result';
+import { DocReadResult } from './components/ai-tools/doc-read-result';
+import { DocSemanticSearchResult } from './components/ai-tools/doc-semantic-search-result';
import { ToolCallCard } from './components/ai-tools/tool-call-card';
import { ToolFailedCard } from './components/ai-tools/tool-failed-card';
import { ToolResultCard } from './components/ai-tools/tool-result-card';
@@ -208,6 +211,9 @@ export function registerAIEffects() {
customElements.define('tool-call-card', ToolCallCard);
customElements.define('tool-result-card', ToolResultCard);
customElements.define('tool-call-failed', ToolFailedCard);
+ customElements.define('doc-semantic-search-result', DocSemanticSearchResult);
+ customElements.define('doc-keyword-search-result', DocKeywordSearchResult);
+ customElements.define('doc-read-result', DocReadResult);
customElements.define('web-crawl-tool', WebCrawlTool);
customElements.define('web-search-tool', WebSearchTool);
customElements.define('doc-compose-tool', DocComposeTool);
diff --git a/packages/frontend/core/src/desktop/pages/workspace/chat/index.tsx b/packages/frontend/core/src/desktop/pages/workspace/chat/index.tsx
index c8643ca44..fe2dd9233 100644
--- a/packages/frontend/core/src/desktop/pages/workspace/chat/index.tsx
+++ b/packages/frontend/core/src/desktop/pages/workspace/chat/index.tsx
@@ -306,6 +306,38 @@ export const Component = () => {
return () => sub.unsubscribe();
}, [framework, mockStd]);
+ // restore pinned session
+ useEffect(() => {
+ if (!chatContent) return;
+
+ const controller = new AbortController();
+ const signal = controller.signal;
+ client
+ .getSessions(
+ workspaceId,
+ {},
+ undefined,
+ { pinned: true, limit: 1 },
+ signal
+ )
+ .then(sessions => {
+ if (!Array.isArray(sessions)) return;
+ const session = sessions[0];
+ if (!session) return;
+ setCurrentSession(session);
+ if (chatContent) {
+ chatContent.session = session;
+ chatContent.reloadSession();
+ }
+ })
+ .catch(console.error);
+
+ // abort the request
+ return () => {
+ controller.abort();
+ };
+ }, [chatContent, client, workspaceId]);
+
const onChatContainerRef = useCallback((node: HTMLDivElement) => {
if (node) {
setIsBodyProvided(true);