Skip to content

Ai mode - #1088

Merged
yattias merged 36 commits into
mainfrom
ai-mode
Sep 11, 2026
Merged

Ai mode#1088
yattias merged 36 commits into
mainfrom
ai-mode

Conversation

@yattias

@yattias yattias commented Sep 8, 2026

Copy link
Copy Markdown
Member

No description provided.

yattias and others added 24 commits September 6, 2026 08:20
The chat transcript, composer, activity feed and model controls are shared
between the notebook assistant and the upcoming AI Mode overlay, so they
live one level up instead of under Notebook/.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
useNotebookChat, useNotebookChatList and useNotebookChatSocket took a
noteId and called NotebookChatService directly. They now take a
ChatTransport, an interface over the six REST calls and the socket URL,
with notebookChatTransport(noteId) and assistantChatTransport() as the two
implementations. The notebook panel builds its transport once per note, so
its behaviour is unchanged.

Adds AssistantChatService against /api/research_ai/assistant/chats/, the
ASSISTANT_CHAT socket route, and the optional notes field the assistant
surface returns on a chat.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
AIModeProvider owns the overlay: ?ai=1 opens it and ?ai=1&aiChat=<id>
selects a conversation, so a reload lands on the same place and any
client-side navigation closes it by dropping the params. The overlay body
is lazy-loaded, sits at z-9500 below modals and tooltips, locks body
scroll, and closes on Esc unless a real modal is showing.

The sidebar gains an Assistant item that toggles the overlay in place.
The three panes are placeholders for now.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The left pane lists assistant conversations newest first with a spinner on
running turns, a Document badge for chats whose detail has been loaded, and
rename behind a row menu. The middle pane composes ChatTranscript and
ChatComposer with the shared model controls, an empty state with static
starter prompts, and autoscroll that stays put once the reader scrolls up.

A conversation is only created on the first send. The list polls every 5s
while any row reports an active turn and refreshes when the open chat's
turn status or title changes. Below the tablet breakpoint the list opens
in a bottom drawer.

Send outcomes now carry the error code and body, so a 429 shows the
budget reset time and a 409 usage_work_in_progress gets its own copy. The
list hook exposes the server's detail on access failures, rendered
verbatim instead of hiding the surface.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The right pane opens by itself when a conversation gains a note (notes[0]
on the representation, or a succeeded create_note in the activity) and
renders the note read-only in the block editor. It refetches when either
signal reports a newer version: a succeeded edit_note carrying
note_version_id, or a note_version_created frame on the note's socket.
Version ids are compared, never assumed ordered, so duplicate and
reordered events are harmless.

While the model composes an edit_note the stream's tool_draft prose is
appended below the settled content as the section being written; when a
turn runs with no draft streaming, an in-progress row shows phase.label so
the page never sits frozen. The badge counts level 1 and 2 headings plus
one for an open draft. An Open in notebook link deep-links via the note's
organization slug. Below the tablet breakpoint the pane is a bottom drawer.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…azily

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Mirrors the notebook's two writing presets so a funder can start a request
for proposals and a researcher a proposal from the empty state, alongside
the research and funding-search starters. Each prefills the composer.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The notebook chat panel owned ~400 lines that turn a newer agent-authored
note version into an in-note diff review: hearing versions from the note
socket, the chat's activity and a reconnect probe; fetching the pinned
version; building or folding the overlay; persisting the accept or reject
projection; and the locks and epochs guarding all of it across note
switches. That logic now lives in useNoteAgentReview, keyed on the note,
its editor, the loaded version and the open chat, so the upcoming AI Mode
document pane can run the same review over its own editor.

The failure banner moves to NoteReviewBanner for the same reason. The
panel keeps the controls placement and the onReviewChange handoff to the
note page; behaviour is unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ts read-only

BlockEditor gains an autofocus prop (default: editable) so a second editor
on the page, like the AI Mode document, doesn't steal focus from the chat
composer. useBlockEditor no longer assigns window.editor, which clobbered
the notebook's editor whenever another editor mounted; nothing read it.
The text bubble menu no longer shows while the editor is read-only via
setEditable(false).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The document pane now mounts the real editor editable, autosaving through
useUpdateNote with the review's persistable projection, and runs
useNoteAgentReview over it: every version the assistant writes is spliced
into the live editor as highlighted insertions and struck removals with
accept/reject controls, exactly as in the notebook. The editor is read-only
while a turn is drafting or working, so a user edit can't make the
assistant's next edit_note stale mid-turn, and editable once settled. The
section badge counts headings in the live editor document.

The note is loaded once per note for the editor's initial content; later
versions reach it through the review rather than a reload. Below the
tablet breakpoint the pane mounts in the drawer only, read-only, so a
note never has two editors open at once.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A card seated under the turn that created the document (or trailing the
transcript when that turn's activity isn't loaded) names it, shows whether
the assistant is still writing, and opens it: the pane beside the chat on
desktop, the swipeable drawer on mobile. The pane still opens by itself on
desktop when a conversation gains a note; on mobile it never does, the card
is the way in.

ChatTranscript gains a renderExecutionExtra slot for host-specific content
under a turn, and the tool-call activity type declares the note_id and
note_title a succeeded create_note carries.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The drawer portals to the body at z-1001, which put it behind the AI Mode
overlay at z-9500 — opening the conversation list or the document on
mobile showed nothing. The drawer takes a zIndex now (default unchanged),
and AI Mode opens its drawers at 9600, below BaseModal.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…-state copy

tiptap re-applies the editable option it was created with, so toggling
editability with setEditable was undone on the next render and the
document stayed editable while the assistant was writing. BlockEditor
gains a locked prop that folds into the option itself and keeps the editor
instance; the document pane locks while a turn is drafting or working.

The pane and the transcript card stop claiming nothing is written once the
chat's activity reports a succeeded edit_note, the section badge ignores
empty headings, and the draft section no longer nests a div in a paragraph.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The overlay portals to the body, takes focus on open, and marks the other
top-level elements inert while it is open, so nothing behind it — a
notebook editor that autofocuses after its note loads, say — can keep
keyboard focus and swallow keys, including the Escape meant for the
overlay. Layers that mount later (menus, tooltips, modals) stay live. The
overlay's own drawers render inside it via a new SwipeableDrawer
container prop.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Mounting the editor on a note the assistant just created dispatched a
document-changing transaction (UniqueID stamping the schema's empty
heading), which fired update and autosaved an editor-authored first
version. The assistant's first edit_note then arrived with a null expected
version, was rejected as stale, and had to re-read and retry — the red X
on the first Edited the note row. The pane now skips saves while the note
has no written version and the document is empty.

Unlocking at the end of a turn also saved a version, because tiptap's
setEditable emits update by default; the editor hook passes emitUpdate
false, since editability isn't a content change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The app router keeps useSearchParams in sync with native history calls,
and unlike router.replace this neither re-fetches nor re-renders the page
behind the overlay on every chat switch.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…graph-first notes

The document pane takes the larger share once open (58%, 520 to 980px)
and the chat narrows to a column beside it. Conversation rows show only
the title and timestamp; the preview and Document badge are gone.

Opening a settled conversation crashed: the editable editor's custom
document schema requires a leading heading, and an assistant-written
note that opens with a paragraph made UniqueID's id-stamping transaction
throw on load. BlockEditor gains a requireTitle prop (default true, so the
notebook is unchanged) and the AI Mode pane turns it off.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ument

Both side panes now drag: the conversation list between 200 and 440px,
the document from 55% of the viewport down to 420px or up to whatever
leaves the chat its 360px minimum. Widths persist per browser. The logic
lives in a new generic useResizableWidth hook, anchored to either
viewport edge; the notebook's useAgentChatWidth becomes a thin wrapper
over it with identical behaviour. ResizeHandle is the shared handle with
pointer drag and arrow-key nudging.

The document pane drops its gray gutter and card: a white surface with
the page in a centered column.

Opening a conversation used to write an editor-authored version and
rename the note to its first section heading: the editor's creation-phase
id-stamping transaction reached the autosave before the instance was
handed to the pane, and the notebook's title derivation took the first
heading. The pane now ignores updates from an instance it doesn't hold
yet and never derives a title, since the assistant names the note.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… gutter fixes

useJumpToLatest owns sticky-bottom transcript scrolling for both the AI
Mode chat and the notebook panel; the AI Mode chat shows a floating jump
control once the reader scrolls up. ConversationMenu (rename, confirmed
delete) and ConversationTitleField are shared by the sidebar rows and the
chat header; delete goes through a new transport method against the
assistant surface's DELETE endpoint and lands on the new-conversation
screen when it was the open chat.

Sidebar rows: gray selected state, 13px titles. The document editor keeps
the 64px gutter its block handle renders into and the pane clips its own
overflow, so the handle no longer floats over the chat.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The composer clears the moment a message is sent and only refills if the
send fails; a rename shows at once in the sidebar and the chat header,
retires when the listing confirms it, and rolls back if the request fails.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Skeleton placeholders replace the loader circles in the conversation
list, the chat and the document pane, aligned to the real content and
fading into it. The chat header takes its title from the listing while
the chat loads, so a refresh never flashes Untitled; the document toggle
is a right-panel icon; the composer drops its own top border under the
pane's separator.

The document pane loses its title bar and lets the chat header toggle
it. Two guards protect notes from a mounting editor: an empty document is
never autosaved from this surface, and an editor that comes up empty for
a note with text is treated as a failed load and locked, after a note was
found overwritten with an empty version.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The publishing form read four things from NotebookContext: the note, the
editor, and the note's two details-saver functions. PublishingHost is a
narrow context carrying exactly those (plus an optional default work
type); NotebookProvider supplies it from state it already holds, and the
form and the published-status strip read it. The notebook is unchanged.

The AI Mode document pane hosts the same form under a Document / Publish
tab pair, supplying the host from the loaded note, the pane's editor and
a details saver for the note. The editor stays mounted behind the form.
The pane's width floor widens while the form shows, and the conversation's
opening message preselects proposal or RFP for a note with no work type.
Modal and ConfirmPublishModal now stack at 9999, above the overlay.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…, gray sidebar

The overlay is now called ResearchHub AI. The new-conversation screen
greets the user by first name over a centered composer, with three equal
starter cards beneath it that mirror the sidebar's Publish menu (request
for proposal, research proposal) plus a funding search. Picking a card
starts the conversation with its message rather than prefilling the box;
sendText is the shared path for cards and the composer alike.

The sidebar sits on gray-100 with the selected row, hover and skeleton
bars adjusted to read on it; the composer area matches the chat's gray-50.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@yattias
yattias requested a review from a team as a code owner September 8, 2026 20:10
@vercel

vercel Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
web Ready Ready Preview Sep 11, 2026 1:14am UTC

Request Review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 99ebfaeaf9

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

chatId != null && (chat.access === 'not_found' || chat.access === 'unauthorized');
const composerDisabled =
listBlocked || chatUnavailable || (chatId != null && chat.access === 'loading');
const composerBusy = chat.isBusy || creatingChat;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Wait for model selection before enabling send

The composer becomes sendable while modelSelection.status === 'loading'. In that state useAgentModelSelection returns an empty request, so a user who sends immediately after opening AI Mode silently starts and permanently pins the conversation to the server-default model instead of their stored selection. This busy guard should include model loading, as the notebook panel's equivalent guard already does.

Useful? React with 👍 / 👎.

Comment on lines 47 to +50
editable,
autofocus,
locked,
requireTitle,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Hide editor menus while the document is locked

Passing locked only changes Tiptap's editable flag, while BlockEditor continues mounting all mutation-capable menus whenever editable is true. During an active assistant turn, existing selections can therefore leave table, column, image, or block controls available, and those controls dispatch commands programmatically despite DOM editing being disabled, allowing the document to change while it is supposed to be locked and potentially making the assistant edit stale. The menus should also be gated by !locked or individually check editor.isEditable.

Useful? React with 👍 / 👎.

Comment on lines +146 to +150
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key !== 'Escape' || event.defaultPrevented) return;
if (isForeignDialogOpen()) return;
close();
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Let an open drawer consume Escape

When either mobile drawer is open, pressing Escape closes the entire AI Mode overlay as well as the drawer. SwipeableDrawer handles Escape without preventing the event, and this document-level listener ignores dialogs contained inside the overlay, so event.defaultPrevented remains false and close() always runs. Detect an open internal drawer here or have the drawer claim the Escape event so the first press only dismisses the topmost layer.

Useful? React with 👍 / 👎.

The delete confirmation looks up the notes the conversation created when
it opens and, if there are any, adds an unchecked option to delete the
document too; otherwise it stays in the notebook. The option maps to the
assistant surface's DELETE ...?delete_notes=true, which soft-deletes the
note the way the notebook does.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Chat prose, the user bubble, and the composer move from 14px to a new
15px text-md token; the composer's focus ring becomes a gray border.
Assistant text now types out: a clock-paced reveal hook keys progress
per turn so streamed narration and the settled answer it becomes share
one pass instead of typing twice, and the transcript follows the
growing text via a resize observer. Only turns watched live animate.

The shared agent-chat layer drops its notebook naming, since both the
notebook chat and the research assistant use it: types/agentChat,
types/agentModels, hooks/useAgentChat, hooks/useAgentChatSocket, and
the AgentChat/AgentChatListItem types. The note-scoped service,
transport, and socket route keep their notebook names. AgentChatPanel
moves under components/Notebook, where its dependencies live, leaving
components/AgentChat as the shared kit. The model preference storage
key is renamed with it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
# Conflicts:
#	components/AgentChat/CreditMeter.tsx
#	components/Notebook/AgentChatPanel.tsx
#	hooks/useAgentChat.ts
#	hooks/useAgentModelSelection.ts
#	hooks/useAgentModels.ts
#	types/agentModels.ts
yattias and others added 5 commits September 10, 2026 14:42
The model picker becomes a BaseMenu, the Radix dropdown the rest of the
app uses, so keyboard navigation, focus return, outside click, and Escape
come from Radix instead of a hand-rolled outside-click hook and document
listener. The effort panel is not a menu — its pills and slider are
adjusted in place — so it moves onto a new shared Popover wrapper in
components/ui, a thin layer over the Radix popover with the chrome three
feature components already hand-roll. The trigger chip forwards its ref
and reads its open state from Radix's data-state.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Five sort controls each rebuilt the same menu: find the selected label,
draw a trigger, list the options as radio rows, close on pick. SortMenu
now does that once, on BaseMenu's new radio group and radio item, and the
journal, peer-review, grant, and proposal feeds use it. MenuTrigger is
the icon-label-chevron button those menus and the composer share, with
chip, inline, and touch variants; ChoicePills and FieldLabel take the
pill row and the small uppercase caption out of ModelControls.

The chat picker moves onto BaseMenu, and the three moderator popovers
onto the shared PopoverContent. The feed sort dropdown is replaced at its
two call sites; the inline dropdown and the nav pill dropdown had no
consumers and are deleted.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Search results live in the top-bar modal now; the /search page was an
artifact. Gone with it: the full-search hook, the sort controls and empty
state, the full-search service call with its highlight and transform
helpers, the response and filter types, the two sanitizer libs, and the
SortDropdown whose only consumer was the page's sort control.

The route's tendrils are unwound: the modal no longer prefetches, pushes
to, or restores a query from the page; the top-bar button no longer
echoes a page query; the topbar route table, the feed content component,
and the feed-source analytics hook no longer know the route. The feed's
search-highlight plumbing was fed only by the deleted service, so the
searchMetadata field, the highlights prop threaded through the entry and
item components, and the sanitized-HTML rendering branches go too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…fort panel

The backend admits only site moderators and hub editors to the research
assistant, so the sidebar entry now shows only for them, and on mobile
they get an Assistant tab in the Peer Review slot that toggles the
overlay in place. Three call sites each spelled that audience check
differently; isHubEditorOrModerator in utils/permissions is now the one
definition, and the note editor and funding home tabs use it too.

Once a chat's effort is locked, the effort panel only says so. Thinking
and temperature depend on the effort the chat runs at, and offering them
under a lock read as a control that half worked.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Resolve NoteEditorLayout.tsx in favour of main: the notebook assistant is
shown to all signed-in users, so the isHubEditorOrModerator import is gone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@yattias
yattias merged commit c638f94 into main Sep 11, 2026
1 of 3 checks passed
@yattias
yattias deleted the ai-mode branch September 11, 2026 01:13
@sonarqubecloud

Copy link
Copy Markdown

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.

1 participant