This is an automated email from the ASF dual-hosted git repository.

voidmatcha pushed a commit to branch temp/seung-00/assistant-transport-base
in repository https://gitbox.apache.org/repos/asf/zeppelin.git

commit 313372203a53cb49e48d32b9a82402103194f270
Author: YONGJAE LEE <[email protected]>
AuthorDate: Thu Oct 8 16:17:01 2026 +0900

    Define shared assistant UI contracts
---
 .../entities/assistant/model/assistantContract.ts  | 84 ++++++++++++++++++++++
 .../assistant/model/messageHistory.spec.ts         | 63 ++++++++++++++++
 .../src/entities/assistant/model/messageHistory.ts | 37 ++++++++++
 .../src/{public-api.ts => assistant-ui.ts}         | 15 ++--
 .../projects/zeppelin-sdk/src/public-api.ts        |  2 +
 5 files changed, 197 insertions(+), 4 deletions(-)

diff --git 
a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/model/assistantContract.ts
 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/model/assistantContract.ts
new file mode 100644
index 0000000000..588b4293db
--- /dev/null
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/model/assistantContract.ts
@@ -0,0 +1,84 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Data and operations the panel needs to display conversations and run 
replies.
+ * Implementations can use a server connection or local fixtures.
+ */
+export interface AssistantConversation {
+  id: string;
+  title?: string;
+  /** Account that created the conversation, shown as "Started by …". */
+  ownerId?: string;
+  /** Whether the requesting user can send messages to the conversation, as 
the server decides. */
+  canSendMessage?: boolean;
+  /** Last activity (ISO time), shown in the conversation list. */
+  updatedAt?: string;
+}
+export interface AssistantMessage {
+  id: string;
+  role: 'user' | 'assistant';
+  content: string;
+  /** From history: the tools the server called before this answer. Live runs 
report them as events instead. */
+  toolCalls?: AssistantToolCallRef[];
+}
+export interface AssistantToolCallRef {
+  id: string;
+  name: string;
+}
+/** One page of a conversation's history, oldest first. */
+export interface AssistantMessagePage {
+  messages: AssistantMessage[];
+  /** Pass as `before` to load the page before this one; null when this page 
starts the conversation. */
+  earlierCursor: string | null;
+}
+export interface AssistantRunBody {
+  prompt: string;
+}
+export type AssistantRunEvent =
+  | { type: 'run.started'; runId?: string }
+  | { type: 'run.completed'; runId?: string }
+  | { type: 'run.failed'; runId?: string; message: string }
+  | { type: 'message.delta'; messageId: string; delta: string }
+  | { type: 'message.done'; messageId: string; content: string }
+  | { type: 'tool_call.started'; toolCallId: string; name: string }
+  // The server does not repeat the tool name on completion.
+  | { type: 'tool_call.done'; toolCallId: string; name?: string };
+
+export type AssistantRunState = 'idle' | 'running' | 'disconnected';
+
+export interface AssistantTransport {
+  /** Conversations of the note the transport was created for. */
+  listConversations(): Promise<AssistantConversation[]>;
+  createConversation(body: { title?: string }): Promise<AssistantConversation>;
+  deleteConversation(conversationId: string): Promise<void>;
+  /** The latest page of history, or the page before `before` (an 
`earlierCursor`). */
+  getMessages(conversationId: string, before?: string): 
Promise<AssistantMessagePage>;
+  /** Observe a retained request, including lost connections while its server 
status is being recovered. */
+  subscribeRunState?(conversationId: string, listener: (state: 
AssistantRunState) => void): () => void;
+  openRun(conversationId: string, body: AssistantRunBody, signal: 
AbortSignal): AsyncIterable<AssistantRunEvent>;
+}
+
+export class AssistantStreamError extends Error {
+  constructor() {
+    super('The assistant stopped responding before the reply finished. Retry 
the request.');
+    this.name = 'AssistantStreamError';
+  }
+}
+
+/** The notebook WebSocket closed during a run; its events went with it, so 
only a reload shows where it got to. */
+export class AssistantConnectionError extends Error {
+  constructor() {
+    super('The connection to the server was lost. Retry to reload the 
conversation and see where the answer got to.');
+    this.name = 'AssistantConnectionError';
+  }
+}
diff --git 
a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/model/messageHistory.spec.ts
 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/model/messageHistory.spec.ts
new file mode 100644
index 0000000000..f93216f700
--- /dev/null
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/model/messageHistory.spec.ts
@@ -0,0 +1,63 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { describe, expect, it } from 'vitest';
+
+import type { AssistantMessage } from './assistantContract';
+import { joinPages } from './messageHistory';
+
+describe('joinPages', () => {
+  it('joins a reply split across pages under the later id and retains both 
action logs', () => {
+    const earlier: AssistantMessage[] = [
+      { id: 'u1', role: 'user', content: 'Read this notebook' },
+      { id: 'a1', role: 'assistant', content: 'Looking.', toolCalls: [{ id: 
'c1', name: 'read' }] }
+    ];
+    const later: AssistantMessage[] = [
+      { id: 'a2', role: 'assistant', content: 'Done.', toolCalls: [{ id: 'c2', 
name: 'summarize' }] }
+    ];
+    expect(joinPages(earlier, later)).toEqual([
+      earlier[0],
+      {
+        id: 'a2',
+        role: 'assistant',
+        content: 'Looking.\n\nDone.',
+        toolCalls: [
+          { id: 'c1', name: 'read' },
+          { id: 'c2', name: 'summarize' }
+        ]
+      }
+    ]);
+    expect(earlier[1].content).toBe('Looking.');
+    expect(later[0].content).toBe('Done.');
+  });
+
+  it('retains a tool-only reply across an empty intermediate page and 
deduplicates repeated calls', () => {
+    const call = { id: 'c1', name: 'read' };
+    const earlier: AssistantMessage[] = [{ id: 'a1', role: 'assistant', 
content: '', toolCalls: [call] }];
+    const later: AssistantMessage[] = [{ id: 'a2', role: 'assistant', content: 
'Done.', toolCalls: [call] }];
+    expect(joinPages(joinPages(earlier, []), later)).toEqual([
+      { id: 'a2', role: 'assistant', content: 'Done.', toolCalls: [call] }
+    ]);
+  });
+
+  it('keeps separate turns apart and handles empty history', () => {
+    const earlier: AssistantMessage[] = [{ id: 'a1', role: 'assistant', 
content: 'First answer' }];
+    const later: AssistantMessage[] = [
+      { id: 'u2', role: 'user', content: 'Another question' },
+      { id: 'a2', role: 'assistant', content: 'Another answer' }
+    ];
+    expect(joinPages(earlier, later)).toEqual([...earlier, ...later]);
+    expect(joinPages([], later)).toEqual(later);
+    expect(joinPages(earlier, [])).toEqual(earlier);
+    expect(joinPages([], [])).toEqual([]);
+  });
+});
diff --git 
a/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/model/messageHistory.ts
 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/model/messageHistory.ts
new file mode 100644
index 0000000000..dce36c3f62
--- /dev/null
+++ 
b/zeppelin-web-angular/projects/zeppelin-react/src/entities/assistant/model/messageHistory.ts
@@ -0,0 +1,37 @@
+/*
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type { AssistantMessage } from './assistantContract';
+
+export const mergeAnswers = (earlier: AssistantMessage, later: 
AssistantMessage): AssistantMessage => {
+  const toolCalls = Array.from(
+    new Map([...(earlier.toolCalls ?? []), ...(later.toolCalls ?? 
[])].map(call => [call.id, call])).values()
+  );
+  return {
+    ...later,
+    content: [earlier.content, later.content].filter(Boolean).join('\n\n'),
+    ...(toolCalls.length ? { toolCalls } : {})
+  };
+};
+
+/**
+ * Puts an earlier page in front of the loaded history. A turn split across 
the page boundary
+ * becomes one bubble again, under the later id.
+ */
+export const joinPages = (earlier: AssistantMessage[], later: 
AssistantMessage[]): AssistantMessage[] => {
+  const last = earlier[earlier.length - 1];
+  const first = later[0];
+  if (last?.role === 'assistant' && first?.role === 'assistant') {
+    return [...earlier.slice(0, -1), mergeAnswers(last, first), 
...later.slice(1)];
+  }
+  return [...earlier, ...later];
+};
diff --git a/zeppelin-web-angular/projects/zeppelin-sdk/src/public-api.ts 
b/zeppelin-web-angular/projects/zeppelin-sdk/src/assistant-ui.ts
similarity index 52%
copy from zeppelin-web-angular/projects/zeppelin-sdk/src/public-api.ts
copy to zeppelin-web-angular/projects/zeppelin-sdk/src/assistant-ui.ts
index ba14a13c7e..8b10321d82 100644
--- a/zeppelin-web-angular/projects/zeppelin-sdk/src/public-api.ts
+++ b/zeppelin-web-angular/projects/zeppelin-sdk/src/assistant-ui.ts
@@ -10,7 +10,14 @@
  * limitations under the License.
  */
 
-export * from './angular-object';
-export * from './interfaces/public-api';
-export * from './message';
-export * from './paragraph-state';
+/** The notebook sidebar's width: its default and its resize range. The 
assistant panel shares it. */
+export const NOTEBOOK_SIDEBAR_WIDTH = { initial: 370, min: 280, max: 800 } as 
const;
+
+/** Result of a host reveal: scrolled to it, already visible, or not rendered. 
*/
+export type AssistantRevealResult = 'shown' | 'visible' | 'missing';
+
+/** A notebook paragraph as the remote sees it: enough to label and link it. */
+export interface AssistantParagraphRef {
+  id: string;
+  title?: string;
+}
diff --git a/zeppelin-web-angular/projects/zeppelin-sdk/src/public-api.ts 
b/zeppelin-web-angular/projects/zeppelin-sdk/src/public-api.ts
index ba14a13c7e..dcf432e844 100644
--- a/zeppelin-web-angular/projects/zeppelin-sdk/src/public-api.ts
+++ b/zeppelin-web-angular/projects/zeppelin-sdk/src/public-api.ts
@@ -11,6 +11,8 @@
  */
 
 export * from './angular-object';
+export * from './assistant-ui';
 export * from './interfaces/public-api';
 export * from './message';
+
 export * from './paragraph-state';

Reply via email to