Skip to content

[ZEPPELIN-6741] Define shared AI Assistant UI contracts and message history behavior - #5561

Merged
tbonelee merged 10 commits into
apache:masterfrom
voidmatcha:voidmatcha/assistant-v2-3-contracts
Oct 16, 2026
Merged

tbonelee merged 10 commits into
apache:masterfrom
voidmatcha:voidmatcha/assistant-v2-3-contracts

Conversation

@voidmatcha

@voidmatcha voidmatcha commented Oct 9, 2026 •

Copy link
Copy Markdown
Member

What is this PR for?

Define the shared types and message history behavior used by the Assistant UI and its host. This gives subsequent UI work a common contract to build against.

Depends on #5558.

What type of PR is it?

Improvement

Todos

What is the Jira issue?

ZEPPELIN-6741

How should this be tested?

Run from zeppelin-web-angular/:

npm run build-project:sdk
npm run typecheck:react
npm --prefix projects/zeppelin-react test -- src/entities/assistant/model/messageHistory.spec.ts

Screenshots (if appropriate)

N/A

Questions:

  • Does the license files need to update? No
  • Is there breaking changes for older versions? No
  • Does this needs documentation? No

@voidmatcha
voidmatcha changed the base branch from temp/voidmatcha/assistant-prerequisites to branch-voidmatcha-assistant-prerequisites-temp October 9, 2026 01:35
@voidmatcha voidmatcha closed this Oct 9, 2026
@voidmatcha voidmatcha reopened this Oct 9, 2026
@voidmatcha

voidmatcha commented Oct 9, 2026 •

Copy link
Copy Markdown
Member Author

This PR defines the types shared by the Assistant UI and its host, and adds the conversion from stored message history to visible UI messages. The current diff is +211/-0 across five files. Excluding 63 lines in *.spec.* tests, the review diff is +148/-0. There is no lockfile change.

This depends on #5558. Review and approval can happen now, but I will wait for #5558 to merge, then rebase this branch onto the latest master and retarget the PR to master before merging it.

@voidmatcha
voidmatcha force-pushed the branch-voidmatcha-assistant-prerequisites-temp branch from 4235999 to 95bd1b7 Compare October 10, 2026 13:40
@voidmatcha
voidmatcha force-pushed the voidmatcha/assistant-v2-3-contracts branch from 89a0192 to 80efed9 Compare October 10, 2026 13:40
@voidmatcha voidmatcha changed the title [ZEPPELIN-6741] Define shared Assistant UI contracts and message history behavior [ZEPPELIN-6741] Define shared AI Assistant UI contracts and message history behavior Oct 10, 2026
@voidmatcha
voidmatcha force-pushed the branch-voidmatcha-assistant-prerequisites-temp branch from 95bd1b7 to 397a5df Compare October 11, 2026 11:30
@voidmatcha
voidmatcha force-pushed the voidmatcha/assistant-v2-3-contracts branch from 699239b to 3e95adf Compare October 11, 2026 11:30
@voidmatcha
voidmatcha changed the base branch from branch-voidmatcha-assistant-prerequisites-temp to master October 15, 2026 14:03
Give the sidebar shell and transport one SDK type to consume without duplicating its declaration in their separate PRs.

Confidence: high
Scope-risk: requires dependent PR branches to synchronize
Not-tested: standalone contracts build in this temporary worktree
@voidmatcha
voidmatcha force-pushed the voidmatcha/assistant-v2-3-contracts branch from 3e95adf to 51fac8d Compare October 15, 2026 14:09
@voidmatcha

Copy link
Copy Markdown
Member Author

Rebased onto master after #5558 merged to remove duplicated prerequisite changes. This PR now contains only its five intended files. The relevant local checks passed.

{ id: 'u2', role: 'user', content: 'Another question' },
{ id: 'a2', role: 'assistant', content: 'Another answer' }
];
expect(joinPages(earlier, later)).toEqual([...earlier, ...later]);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The most common page boundary is a question at the end of the earlier page and its answer at the start of the later one, but no test covers it. All tests still pass after removing last?.role === 'assistant' && from joinPages, which would merge the question into the answer. How about adding this case?

Suggested change
expect(joinPages(earlier, later)).toEqual([...earlier, ...later]);
expect(joinPages(earlier, later)).toEqual([...earlier, ...later]);
expect(joinPages([later[0]], [later[1]])).toEqual(later);

Comment on lines +27 to +28
/** From history: the tools the server called before this answer. Live runs report them as events instead. */
toolCalls?: AssistantToolCallRef[];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When one answer spans several rounds (text, tool, text, tool), this shape merges them into a single content and a single toolCalls, so the order between text and tools is lost. The server history keeps that order, and the Direction in the frontend proposal (a renderer per tool inside the action log, the approval flow) may later need the answer shown step by step. Changing it then would change the shape of this contract, and the transport and UI along with it. How about keeping each round as segments instead of merging them, and joining them in the current UI? If merging is intended, could you share why?

Comment thread zeppelin-web-angular/projects/zeppelin-sdk/src/public-api.ts Outdated
@voidmatcha
voidmatcha force-pushed the voidmatcha/assistant-v2-3-contracts branch from f114b93 to 6e44f7b Compare October 15, 2026 14:30
@voidmatcha

Copy link
Copy Markdown
Member Author

Thanks for the review. I updated the history model to preserve Assistant rounds as ordered segments, matching how the server stores them. I also removed the name field from tool_call.done, which the server does not send, and added tests for a question/answer split across page boundaries and overlapping pages. The focused unit tests, TypeScript check, and ESLint passed locally. The follow-up transport PR #5563 has been updated to use the same contract.

Comment on lines +33 to +37
/** Current UI projections; render segments to show text and actions in their original order. */
content: string;
toolCalls?: AssistantToolCallRef[];
/** Ordered assistant rounds as stored by the server, including tool-only rounds. */
segments: AssistantMessageSegment[];

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

content and toolCalls can be derived from segments, so storing them as well lets them drift apart, as the input of the overlapping page test already does. How about dropping both fields, keeping only the rounds, and deriving them where needed?

export interface AssistantRound { id: string; content: string; toolCalls: AssistantToolCallRef[] }
export interface AssistantAnswerMessage { id: string; role: 'assistant'; rounds: AssistantRound[] }
export const answerText = (answer: AssistantAnswerMessage) =>
  answer.rounds.map(round => round.content).filter(Boolean).join('\n\n');

Comment on lines +15 to +21
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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do these fields need to be optional?

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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be cursor?

See MessagesResponse.java.

earlierCursor: string | null;
}
export interface AssistantRunBody {
prompt: string;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The server currently expects this field to be named content.

getMessages(conversationId: string, before?: string): Promise<AssistantMessagePage>;
/** Observe local run state, including a lost connection until history is reloaded. */
subscribeRunState?(conversationId: string, listener: (state: AssistantRunState) => void): () => void;
openRun(conversationId: string, body: AssistantRunBody, signal: AbortSignal): AsyncIterable<AssistantRunEvent>;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like cancellation isn't supported on the server side.

tbonelee
tbonelee previously approved these changes Oct 15, 2026
@voidmatcha
voidmatcha force-pushed the voidmatcha/assistant-v2-3-contracts branch from da1d64c to 290cae6 Compare October 15, 2026 15:43
content: string;
toolCalls?: AssistantToolCallRef[];
}
/** One page of a conversation's history, oldest first. */

@seung-00 seung-00 Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The server’s listMessages() returns messages newest first, but this comment says “oldest first.” Should these match?
For example, assuming m5 is the newest message, the server returns [m5, m4, m3].

@voidmatcha

Copy link
Copy Markdown
Member Author

@seung-00 Thank you for the thorough review. I removed the duplicated fields and aligned the request and response contracts with the server. #5563 converts the server’s newest-first messages to chronological order for the UI. The ordering and client-only abort behavior are documented and covered by tests.

Comment on lines +54 to +61
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 }
| { type: 'tool_call.done'; toolCallId: string };

@seung-00 seung-00 Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just for reference, the server wraps the event data in payload. See AssistantEventPayload

Also, run.failed provides an error status rather than a message field.
For example:

{
  "conversationId": "...",
  "type": "run.failed",
  "payload": {
    "runId": "...",
    "error": {
      "status": 500
    }
  }
}

We could add a code for more specific handling later if needed.

@voidmatcha voidmatcha Oct 15, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the clarification. AssistantRunEvent is the UI-facing event, not the raw WebSocket event. The follow-up transport in #5563 reads payload.error.status and maps it to the message field; this is covered by its tests.

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';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When would this type be used?

@voidmatcha voidmatcha Oct 15, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This type is for the revealParagraph callback planned in ZEPPELIN-6749. The Angular host would report whether a linked paragraph was scrolled into view, was already visible, or could not be found, and the React panel would announce the missing case. Since it is not consumed in this PR, I removed the declaration here and will define the result type alongside that callback.

return {
...later,
segments
};

@seung-00 seung-00 Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we make the return value more explicit? Spreading later could ignore some of earlier’s fields, such as toolCalls.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the careful review. toolCalls live on each segment, so the existing merge retains the calls from both pages. I agree that the top-level merge policy should be explicit, so I replaced ...later with id, role, and segments. All five message-history tests pass.

@tbonelee
tbonelee merged commit c10a4a8 into apache:master Oct 16, 2026
24 checks passed
@tbonelee

Copy link
Copy Markdown
Contributor

Merged into master (c10a4a8).

tbonelee pushed a commit that referenced this pull request Oct 18, 2026
…debar

### What is this PR for?

Add an opt-in AI Assistant button and empty panel shell to the notebook sidebar. With `?reactAssistant=true`, the Angular notebook mounts the React shell, coordinates it with the file tree and table of contents, and shares the sidebar width for resizing. The panel closes when the active note changes and reports the failure and removes the button if the React remote fails to load.

This establishes the notebook mount point for later conversation UI work. Conversation components, server transport, and message handling are outside this PR.

#5558 and #5561 have merged. This PR is now based on master.

### What type of PR is it?

Feature

### Todos

* [x] Merge #5558 and #5561 before this PR
* [x] Rebase onto master and retarget this PR to master before merging
* [x] Confirm CI results

### What is the Jira issue?

ZEPPELIN-6743

### How should this be tested?

Run from `zeppelin-web-angular/`:

```bash
npm run build-project:sdk
npm run typecheck:react
npm --prefix projects/zeppelin-react test -- src/pages/AssistantWorkspace.spec.tsx
npm run test:shell -- src/app/pages/workspace/notebook/assistant src/app/pages/workspace/notebook/sidebar/sidebar.component.spec.ts src/app/services/react-feature.service.spec.ts
npm run lint
npm run build:react
```

In an authenticated notebook, open `?reactAssistant=true` and check opening, closing, resizing, switching sidebar sections, and switching notes. Confirm that the button is absent without the flag and in view-only and revision views.

### Screenshots (if appropriate)

N/A

### Questions:

* Does the license files need to update? No
* Is there breaking changes for older versions? No
* Does this needs documentation? No




Closes #5565 from voidmatcha/voidmatcha/assistant-sidebar-shell.

Signed-off-by: ChanHo Lee <chanholee@apache.org>
tbonelee pushed a commit that referenced this pull request Oct 18, 2026
…T and WebSocket APIs

### What is this PR for?

Connect the Assistant frontend to the conversation REST and WebSocket APIs. The transport provides conversation creation and retrieval, message sending, and response delivery for the UI.

Server PR #5547, #5558, and UI contracts #5561 have merged. This PR is now based on master.

### What type of PR is it?

Feature

### Todos

* [x] Merge #5547
* [x] Merge #5558
* [x] Merge #5561 before this PR
* [x] Rebase onto master, retarget this PR to master, and rerun CI before merging
* [x] Compare against the final API contract from server PR #5547
* [x] Confirm CI on the current PR head

### What is the Jira issue?

ZEPPELIN-6742

### How should this be tested?

Run from `zeppelin-web-angular/`:

```bash
npm run build-project:sdk
npm run typecheck:react
npm --prefix projects/zeppelin-react test -- src/entities/assistant/model/assistantTransport.spec.ts
```

Also verify the REST and WebSocket integration against the final API contract from server PR #5547.

### Screenshots (if appropriate)

N/A

### Questions:

* Does the license files need to update? No
* Is there breaking changes for older versions? No
* Does this needs documentation? No


Closes #5563 from voidmatcha/voidmatcha/assistant-v2-6-transport.

Signed-off-by: ChanHo Lee <chanholee@apache.org>
voidmatcha pushed a commit that referenced this pull request Oct 18, 2026
### What is this PR for?

Add `ConversationList` and `ConversationHeader`, the Assistant panel's conversation browsing UI, reviewable in Storybook without a server.

**Components**
* `ConversationList`
  * Takes conversation data and the selection state as props
  * Shows the list, search results, and an empty state
  * Shows "No conversations match your search." while a search is active and "No conversations yet." otherwise, so the message stays correct when the panel passes an already-filtered list
* `ConversationHeader`
  * Shows the inner header for the selected conversation
* Both report new conversation, selection, delete requests, and search term changes through callbacks
* Deletion and its confirmation step are left to panel assembly (ZEPPELIN-6748)

**Beyond the acceptance criteria**
* The list groups conversations into "Today" (with a relative time) and "Earlier", within the issue's design latitude
* Search shows a flat result list without sections
* Read-only conversations (`canSendMessage === false`) show a lock and no delete button, and the read-only state is announced to screen readers

**Title handling**
* `title` is optional in the contract, and the server preserves an explicit empty title on create
* A missing or empty title is shown as "Untitled conversation" in the list, the header, the row button name, and both delete labels, through a shared `conversationTitle` helper
* Search matches the displayed title, so an untitled conversation is found by "untitled"

**Dependencies**
* #5561 is merged and this branch is rebased onto master
* Depends on #5565 (ZEPPELIN-6743) per Jira

### What type of PR is it?

Feature

### Todos

* [x] Add `ConversationList` with search, distinct empty states, "Today"/"Earlier" sections, and delete requests
* [x] Add `ConversationHeader` for the selected conversation
* [x] Hide the delete button for read-only conversations
* [x] Expose the read-only state and relative time to screen readers
* [x] Show "Untitled conversation" for a missing or empty title, and match it in search
* [x] Key the empty-result message on whether a search is active
* [x] Check the second conversation's ID in the select and delete tests and story
* [x] Add stories for the empty list, search, and select and delete, plus a selected conversation, "Today"/"Earlier", and the header states
* [x] Add unit tests for filtering, sections, callbacks, the fallback title, the read-only marker, and relative time formats
* [x] Rebase onto master and fill the fields the master contract requires in fixtures

### What is the Jira issue?

ZEPPELIN-6747

### How should this be tested?

Run from `zeppelin-web-angular/projects/zeppelin-react/`:

```bash
npm ci
npm test
npm run typecheck
npm run lint
npm run build:storybook
npm run test:storybook
npm run storybook
```

`npm test` runs 22 files and 208 tests, 26 of them new. `npm run test:storybook` runs every story in both themes (40 tests) and needs Playwright Chromium. In Storybook, check `Assistant/ConversationList` (Empty, Search, Selected, SelectAndDelete, TodayAndEarlier) and `Assistant/ConversationHeader` (Current, NoSelection, ReadOnly) in both the light and dark themes.

### Screenshots (if appropriate)

N/A

### Questions:

* Does the license files need to update? No
* Is there breaking changes for older versions? No
* Does this needs documentation? No

Closes #5575 from xhaktm00/ZEPPELIN-6747.

Signed-off-by: YONGJAE LEE <dev.yongjaelee@gmail.com>
voidmatcha pushed a commit that referenced this pull request Oct 20, 2026
### What is this PR for?

Add the AI Assistant message and answer state UI: user and Assistant messages are shown distinctly, and an answer displays its loading, streaming, tool-use, completed and error states. Everything is driven by fixture props. There is no server, transport or panel state. The components live in `projects/zeppelin-react/src/entities/assistant/ui`.

This PR adds new files under `entities/assistant/ui` and removes the temporary `Assistant/Setup` story. It builds on the shared contract from ZEPPELIN-6741 (#5561) and the Markdown answer from ZEPPELIN-6744 (#5570), both on `master`.

Commits:
1. **Message item and answer view types**
   * `AssistantMessageItem` renders a question or an answer:
     * A question is a right-aligned bubble, always shown as typed. It never goes through the content renderer.
     * An answer is a full-width body under an "AI / Assistant" label. While it streams, it is marked `aria-busy` and shows a caret where the text ends.
   * `AssistantAnswerView` is the display union (`loading` / `streaming` / `done` / `error`). It reuses `AssistantAnswerMessage` and `AssistantToolCallRef` from the ZEPPELIN-6741 contract.
2. **Answer status, tool log and error displays**
   * `AssistantAnswer` renders one view at a time. Each state fully replaces the last.
   * The progress line (`role="status"`) reads "Thinking…", "Writing answer…" or "Running \<tool\>…".
   * `AssistantToolLog` is open while a tool runs and collapses once the answer completes. A tool without a name is labelled "Tool".
   * `AssistantAnswerError` is an antd `Alert` (`role="alert"`):
     * `failed` (`run.failed`) and `stream` (`AssistantStreamError`) are errors with **Retry**.
     * `connection` (`AssistantConnectionError`) is a warning with **Reload conversation**, because the answer may have finished on the server.
     * With no `onRetry`, no button is shown.
     * A partial answer that arrived before the error is kept.
3. **Stories**
   * `Assistant/Message`: User, CompletedReply, StreamingReply, LongContent, Conversation.
   * `Assistant/Status`: Loading, Streaming, ToolRunning, Completed, CompletedWithTools, Failed, StreamError, ConnectionLost, ErrorWithoutRetry.
   * `Assistant/Status/StateSwitcher` switches the fixture from the Controls panel, so you can check that the states stay consistent in both themes.
4. **Review: remove the temporary theme check story**
   * `Assistant/Setup › TemporaryThemeCheck` (`AssistantScope.stories.tsx`) was a placeholder until the Assistant UI stories landed. Nothing else referenced it.
5. **Review: align the answer UI with segmented history** (by <at>voidmatcha)
   * A completed answer is an `AssistantAnswerMessage` with ordered `segments`. Its text joins the rounds in order, and its tool log lists the tools from every round.
   * Tools still marked running when an error arrives are shown as "status unknown", not as running or done.
6. **Review: render answers as Markdown by default**
   * `renderContent` now defaults to `MarkdownAnswer` from ZEPPELIN-6744. A parent can still pass its own, for example one that resolves paragraph references.
   * `MarkdownAnswer` wraps the text in blocks, so the streaming caret is now drawn inside the last block (or the last item of a closing list). It stays on the line where the text ends instead of dropping below it.

Styling uses `<at>vanilla-extract/css` through the Assistant theme contract only, so light and dark need no per-component rules. The HTML/CSS screens in `voidmatcha/assistant-storybook-share` were used as a design reference. The code is written from scratch.

**Out of scope** (per the issue / ZEPPELIN-6748): run event → view derivation, panel mounting in `AssistantWorkspace`, real tool calls, approval, persistence and WebSocket handling. Showing failed tool calls (raised in review) is tracked in ZEPPELIN-6754.

### What type of PR is it?
Feature

### Todos
* [x] Use the ZEPPELIN-6744 Markdown component as the default `renderContent` once it merges

### What is the Jira issue?
* https://issues.apache.org/jira/browse/ZEPPELIN-6745

### How should this be tested?
In `zeppelin-web-angular/projects/zeppelin-react`:
* `npm ci`, `npm test` (254 passed), `npm run typecheck`, `npm run lint` and `npm run build:storybook` all pass.
  * `typecheck` needs `npm ci` in `zeppelin-web-angular` first, because the SDK resolves `rxjs` from there.
  * The new files also pass the frontend's `prettier --check`.
* `npm run storybook`: both themes checked manually for every story above, including StateSwitcher.
* The specs were mutation-checked: making the connection error say "Retry", keeping the progress line on error, dropping the "Tool" fallback, rendering a question through `renderContent`, keeping the tool log open, or going back to a plain-text default each fails exactly the intended test.

**`npm run test:storybook`:**
* With `--workers=1`, all 68 tests pass.
* With the default config (3 workers), 1–5 tests time out between runs.
  * The traces show asset requests to the `python3 -m http.server` web server failing (status -1) and a blank page.
  * On the earlier temporary base (`4599ad2d9`), its own story alone reproduced this: with `--repeat-each=8`, 1–2 of 16 time out.
  * So it is a problem with the existing smoke-test server, not with these stories. I left it untouched here.

### Screenshots (if appropriate)
See the `Assistant/Message` and `Assistant/Status` stories.

### Questions:
* Does the license files need to update? No. There are no new dependencies.
* Is there breaking changes for older versions? No. Only new files are added, plus the removal of a temporary story.
* Does this needs documentation? No.



Closes #5567 from kimyenac/ZEPPELIN-6745.

Signed-off-by: YONGJAE LEE <dev.yongjaelee@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants