From 5dd068b12ab83fca848845aa46cfaea79d93a591 Mon Sep 17 00:00:00 2001 From: Nakul Date: Fri, 8 Aug 2025 12:38:15 +0530 Subject: [PATCH 01/15] adding multichats panel --- packages/jupyter-chat/src/index.ts | 1 + packages/jupyter-chat/src/multiChatPanel.tsx | 387 ++++++++++++++ .../jupyterlab-chat-extension/src/index.ts | 7 +- packages/jupyterlab-chat/src/token.ts | 3 +- packages/jupyterlab-chat/src/widget.tsx | 490 ++---------------- 5 files changed, 424 insertions(+), 464 deletions(-) create mode 100644 packages/jupyter-chat/src/multiChatPanel.tsx diff --git a/packages/jupyter-chat/src/index.ts b/packages/jupyter-chat/src/index.ts index ad7b8f26..aef58aa6 100644 --- a/packages/jupyter-chat/src/index.ts +++ b/packages/jupyter-chat/src/index.ts @@ -13,3 +13,4 @@ export * from './registers'; export * from './selection-watcher'; export * from './types'; export * from './widgets'; +export * from './multiChatPanel'; diff --git a/packages/jupyter-chat/src/multiChatPanel.tsx b/packages/jupyter-chat/src/multiChatPanel.tsx new file mode 100644 index 00000000..21523f61 --- /dev/null +++ b/packages/jupyter-chat/src/multiChatPanel.tsx @@ -0,0 +1,387 @@ +/* + * Multi-chat panel for @jupyter/chat + * Originally adapted from jupyterlab-chat's ChatPanel + */ + +import { + ChatWidget, + IAttachmentOpenerRegistry, + IChatCommandRegistry, + IChatModel, + IInputToolbarRegistry, + IMessageFooterRegistry, + readIcon +} from './index'; +import { Contents } from '@jupyterlab/services'; +import { IThemeManager } from '@jupyterlab/apputils'; +import { PathExt } from '@jupyterlab/coreutils'; +import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; +import { + addIcon, + closeIcon, + CommandToolbarButton, + HTMLSelect, + launchIcon, + PanelWithToolbar, + ReactWidget, + SidePanel, + Spinner, + ToolbarButton +} from '@jupyterlab/ui-components'; +import { CommandRegistry } from '@lumino/commands'; +import { ISignal, Signal } from '@lumino/signaling'; +import { AccordionPanel, Panel } from '@lumino/widgets'; +import React, { useState } from 'react'; + +const SIDEPANEL_CLASS = 'jp-chat-sidepanel'; +const ADD_BUTTON_CLASS = 'jp-chat-add'; +const OPEN_SELECT_CLASS = 'jp-chat-open'; +const SECTION_CLASS = 'jp-chat-section'; +const TOOLBAR_CLASS = 'jp-chat-toolbar'; + +/** + * Generic sidepanel widget including multiple chats and the add chat button. + */ +export class MultiChatPanel extends SidePanel { + constructor(options: ChatPanel.IOptions) { + super(options); + this.addClass(SIDEPANEL_CLASS); + this._commands = options.commands; + this._contentsManager = options.contentsManager; + this._rmRegistry = options.rmRegistry; + this._themeManager = options.themeManager; + this._defaultDirectory = options.defaultDirectory; + this._chatFileExtension = options.chatFileExtension; + this._createChatCommand = options.createChatCommand; + this._openChatCommand = options.openChatCommand; + this._chatCommandRegistry = options.chatCommandRegistry; + this._attachmentOpenerRegistry = options.attachmentOpenerRegistry; + this._inputToolbarFactory = options.inputToolbarFactory; + this._messageFooterRegistry = options.messageFooterRegistry; + this._welcomeMessage = options.welcomeMessage; + + const addChat = new CommandToolbarButton({ + commands: this._commands, + id: this._createChatCommand, + args: { inSidePanel: true }, + icon: addIcon + }); + addChat.addClass(ADD_BUTTON_CLASS); + this.toolbar.addItem('createChat', addChat); + + this._openChat = ReactWidget.create( + + ); + this._openChat.addClass(OPEN_SELECT_CLASS); + this.toolbar.addItem('openChat', this._openChat); + + const content = this.content as AccordionPanel; + content.expansionToggled.connect(this._onExpansionToggled, this); + + this._contentsManager.fileChanged.connect((_, args) => { + if (args.type === 'delete') { + this.widgets.forEach(widget => { + if ((widget as ChatSection).path === args.oldValue?.path) { + widget.dispose(); + } + }); + this.updateChatList(); + } + const updateActions = ['new', 'rename']; + if ( + updateActions.includes(args.type) && + args.newValue?.path?.endsWith(this._chatFileExtension) + ) { + this.updateChatList(); + } + }); + } + + get defaultDirectory(): string { + return this._defaultDirectory; + } + set defaultDirectory(value: string) { + if (value === this._defaultDirectory) { + return; + } + this._defaultDirectory = value; + this.updateChatList(); + this.widgets.forEach(w => { + (w as ChatSection).defaultDirectory = value; + }); + } + + addChat(model: IChatModel): ChatWidget { + const content = this.content as AccordionPanel; + for (let i = 0; i < this.widgets.length; i++) { + content.collapse(i); + } + + let inputToolbarRegistry: IInputToolbarRegistry | undefined; + if (this._inputToolbarFactory) { + inputToolbarRegistry = this._inputToolbarFactory.create(); + } + + const widget = new ChatWidget({ + model, + rmRegistry: this._rmRegistry, + themeManager: this._themeManager, + chatCommandRegistry: this._chatCommandRegistry, + attachmentOpenerRegistry: this._attachmentOpenerRegistry, + inputToolbarRegistry, + messageFooterRegistry: this._messageFooterRegistry, + welcomeMessage: this._welcomeMessage + }); + + this.addWidget( + new ChatSection({ + widget, + commands: this._commands, + path: model.name, + defaultDirectory: this._defaultDirectory, + openChatCommand: this._openChatCommand + }) + ); + + return widget; + } + + updateChatList = async (): Promise => { + const extension = this._chatFileExtension; + this._contentsManager + .get(this._defaultDirectory) + .then((contentModel: Contents.IModel) => { + const chatsNames: { [name: string]: string } = {}; + (contentModel.content as any[]) + .filter(f => f.type === 'file' && f.name.endsWith(extension)) + .forEach(f => { + chatsNames[PathExt.basename(f.name, extension)] = f.path; + }); + this._chatNamesChanged.emit(chatsNames); + }) + .catch(e => console.error('Error getting chat files from drive', e)); + }; + + openIfExists(path: string): boolean { + const index = this._getChatIndex(path); + if (index > -1) { + this._expandChat(index); + } + return index > -1; + } + + protected onAfterAttach(): void { + this._openChat.renderPromise?.then(() => this.updateChatList()); + } + + private _getChatIndex(path: string) { + return this.widgets.findIndex(w => (w as ChatSection).path === path); + } + + private _expandChat(index: number): void { + if (!this.widgets[index].isVisible) { + (this.content as AccordionPanel).expand(index); + } + } + + private _chatSelected = ( + event: React.ChangeEvent + ): void => { + const select = event.target; + const path = select.value; + const name = select.options[select.selectedIndex].textContent; + if (name === '-') { + return; + } + this._commands.execute(this._openChatCommand, { + filepath: path, + inSidePanel: true + }); + event.target.selectedIndex = 0; + }; + + private _onExpansionToggled(panel: AccordionPanel, index: number) { + if (!this.widgets[index].isVisible) { + return; + } + for (let i = 0; i < this.widgets.length; i++) { + if (i !== index) { + panel.collapse(i); + } + } + } + + private _chatNamesChanged = new Signal( + this + ); + private _commands: CommandRegistry; + private _defaultDirectory: string; + private _chatFileExtension: string; + private _createChatCommand: string; + private _openChatCommand: string; + private _contentsManager: Contents.IManager; + private _openChat: ReactWidget; + private _rmRegistry: IRenderMimeRegistry; + private _themeManager: IThemeManager | null; + private _chatCommandRegistry?: IChatCommandRegistry; + private _attachmentOpenerRegistry?: IAttachmentOpenerRegistry; + private _inputToolbarFactory?: ChatPanel.IInputToolbarRegistryFactory; + private _messageFooterRegistry?: IMessageFooterRegistry; + private _welcomeMessage?: string; +} + +export namespace ChatPanel { + export interface IOptions extends SidePanel.IOptions { + commands: CommandRegistry; + contentsManager: Contents.IManager; + rmRegistry: IRenderMimeRegistry; + themeManager: IThemeManager | null; + defaultDirectory: string; + chatFileExtension: string; + createChatCommand: string; + openChatCommand: string; + chatCommandRegistry?: IChatCommandRegistry; + attachmentOpenerRegistry?: IAttachmentOpenerRegistry; + inputToolbarFactory?: IInputToolbarRegistryFactory; + messageFooterRegistry?: IMessageFooterRegistry; + welcomeMessage?: string; + } + export interface IInputToolbarRegistryFactory { + create(): IInputToolbarRegistry; + } +} + +class ChatSection extends PanelWithToolbar { + constructor(options: ChatSection.IOptions) { + super(options); + this.addWidget(options.widget); + this.addWidget(this._spinner); + this.addClass(SECTION_CLASS); + this._defaultDirectory = options.defaultDirectory; + this._path = options.path; + this._openChatCommand = options.openChatCommand; + this._updateTitle(); + this.toolbar.addClass(TOOLBAR_CLASS); + + this._markAsRead = new ToolbarButton({ + icon: readIcon, + iconLabel: 'Mark chat as read', + className: 'jp-mod-styled', + onClick: () => (this.model.unreadMessages = []) + }); + + const moveToMain = new ToolbarButton({ + icon: launchIcon, + iconLabel: 'Move the chat to the main area', + className: 'jp-mod-styled', + onClick: () => { + this.model.dispose(); + options.commands.execute(this._openChatCommand, { + filepath: this._path + }); + this.dispose(); + } + }); + + const closeButton = new ToolbarButton({ + icon: closeIcon, + iconLabel: 'Close the chat', + className: 'jp-mod-styled', + onClick: () => { + this.model.dispose(); + this.dispose(); + } + }); + + this.toolbar.addItem('markRead', this._markAsRead); + this.toolbar.addItem('moveMain', moveToMain); + this.toolbar.addItem('close', closeButton); + + this.model.unreadChanged?.connect(this._unreadChanged); + this._markAsRead.enabled = this.model.unreadMessages.length > 0; + + options.widget.node.style.height = '100%'; + (this.model as any).ready?.then?.(() => { + this._spinner.dispose(); + }); + } + + get path(): string { + return this._path; + } + + set defaultDirectory(value: string) { + this._defaultDirectory = value; + this._updateTitle(); + } + + get model(): IChatModel { + return (this.widgets[0] as ChatWidget).model; + } + + dispose(): void { + this.model.unreadChanged?.disconnect(this._unreadChanged); + super.dispose(); + } + + private _updateTitle(): void { + const inDefault = this._defaultDirectory + ? !PathExt.relative(this._defaultDirectory, this._path).startsWith('..') + : true; + const pattern = new RegExp(`${this._path.split('.').pop()}$`, 'g'); + this.title.label = ( + inDefault + ? this._defaultDirectory + ? PathExt.relative(this._defaultDirectory, this._path) + : this._path + : '/' + this._path + ).replace(pattern, ''); + this.title.caption = this._path; + } + + private _unreadChanged = (_: IChatModel, unread: number[]) => { + this._markAsRead.enabled = unread.length > 0; + }; + + private _defaultDirectory: string; + private _markAsRead: ToolbarButton; + private _path: string; + private _openChatCommand: string; + private _spinner = new Spinner(); +} + +export namespace ChatSection { + export interface IOptions extends Panel.IOptions { + commands: CommandRegistry; + defaultDirectory: string; + widget: ChatWidget; + path: string; + openChatCommand: string; + } +} + +type ChatSelectProps = { + chatNamesChanged: ISignal; + handleChange: (event: React.ChangeEvent) => void; +}; + +function ChatSelect({ + chatNamesChanged, + handleChange +}: ChatSelectProps): JSX.Element { + const [chatNames, setChatNames] = useState<{ [name: string]: string }>({}); + chatNamesChanged.connect((_, names) => setChatNames(names)); + return ( + + + {Object.keys(chatNames).map(name => ( + + ))} + + ); +} diff --git a/packages/jupyterlab-chat-extension/src/index.ts b/packages/jupyterlab-chat-extension/src/index.ts index 7271549d..3bbe3a2a 100644 --- a/packages/jupyterlab-chat-extension/src/index.ts +++ b/packages/jupyterlab-chat-extension/src/index.ts @@ -49,7 +49,6 @@ import { ITranslator, nullTranslator } from '@jupyterlab/translation'; import { launchIcon } from '@jupyterlab/ui-components'; import { PromiseDelegate } from '@lumino/coreutils'; import { - ChatPanel, ChatWidgetFactory, CommandIDs, IActiveCellManagerToken, @@ -65,6 +64,7 @@ import { YChat, chatFileType } from 'jupyterlab-chat'; +import { MultiChatPanel as ChatPanel } from '@jupyter/chat'; import { chatCommandRegistryPlugin } from './chat-commands/plugins'; import { emojiCommandsPlugin } from './chat-commands/providers/emoji'; import { mentionCommandsPlugin } from './chat-commands/providers/user-mention'; @@ -740,7 +740,10 @@ const chatPanel: JupyterFrontEndPlugin = { attachmentOpenerRegistry, inputToolbarFactory, messageFooterRegistry, - welcomeMessage + welcomeMessage, + chatFileExtension: chatFileType.extensions[0], + createChatCommand: CommandIDs.createChat, + openChatCommand: CommandIDs.openChat }); chatPanel.id = 'JupyterlabChat:sidepanel'; chatPanel.title.icon = chatIcon; diff --git a/packages/jupyterlab-chat/src/token.ts b/packages/jupyterlab-chat/src/token.ts index 5b180276..0dd3668d 100644 --- a/packages/jupyterlab-chat/src/token.ts +++ b/packages/jupyterlab-chat/src/token.ts @@ -15,7 +15,8 @@ import { WidgetTracker } from '@jupyterlab/apputils'; import { DocumentRegistry } from '@jupyterlab/docregistry'; import { Token } from '@lumino/coreutils'; import { ISignal } from '@lumino/signaling'; -import { ChatPanel, LabChatPanel } from './widget'; +import { LabChatPanel } from './widget'; +import { MultiChatPanel as ChatPanel } from '@jupyter/chat'; /** * The file type for a chat document. diff --git a/packages/jupyterlab-chat/src/widget.tsx b/packages/jupyterlab-chat/src/widget.tsx index 786c9dc0..13e24361 100644 --- a/packages/jupyterlab-chat/src/widget.tsx +++ b/packages/jupyterlab-chat/src/widget.tsx @@ -8,32 +8,14 @@ import { IAttachmentOpenerRegistry, IChatCommandRegistry, IChatModel, - IInputToolbarRegistry, - IMessageFooterRegistry, - readIcon + IMessageFooterRegistry } from '@jupyter/chat'; +import { MultiChatPanel } from '@jupyter/chat'; import { Contents } from '@jupyterlab/services'; import { IThemeManager } from '@jupyterlab/apputils'; -import { PathExt } from '@jupyterlab/coreutils'; import { DocumentWidget } from '@jupyterlab/docregistry'; import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; -import { - addIcon, - closeIcon, - CommandToolbarButton, - HTMLSelect, - launchIcon, - PanelWithToolbar, - ReactWidget, - SidePanel, - Spinner, - ToolbarButton -} from '@jupyterlab/ui-components'; import { CommandRegistry } from '@lumino/commands'; -import { Message } from '@lumino/messaging'; -import { ISignal, Signal } from '@lumino/signaling'; -import { AccordionPanel, Panel } from '@lumino/widgets'; -import React, { useState } from 'react'; import { LabChatModel } from './model'; import { @@ -44,11 +26,6 @@ import { const MAIN_PANEL_CLASS = 'jp-lab-chat-main-panel'; const TITLE_UNREAD_CLASS = 'jp-lab-chat-title-unread'; -const SIDEPANEL_CLASS = 'jp-lab-chat-sidepanel'; -const ADD_BUTTON_CLASS = 'jp-lab-chat-add'; -const OPEN_SELECT_CLASS = 'jp-lab-chat-open'; -const SECTION_CLASS = 'jp-lab-chat-section'; -const TOOLBAR_CLASS = 'jp-lab-chat-toolbar'; /** * DocumentWidget: widget that represents the view or editor for a file type. @@ -95,442 +72,33 @@ export class LabChatPanel extends DocumentWidget { }; } -/** - * Sidepanel widget including the chats and the add chat button. - */ -export class ChatPanel extends SidePanel { - /** - * The constructor of the chat panel. - */ - constructor(options: ChatPanel.IOptions) { - super(options); - this.addClass(SIDEPANEL_CLASS); - this._commands = options.commands; - this._contentsManager = options.contentsManager; - this._rmRegistry = options.rmRegistry; - this._themeManager = options.themeManager; - this._defaultDirectory = options.defaultDirectory; - this._chatCommandRegistry = options.chatCommandRegistry; - this._attachmentOpenerRegistry = options.attachmentOpenerRegistry; - this._inputToolbarFactory = options.inputToolbarFactory; - this._messageFooterRegistry = options.messageFooterRegistry; - this._welcomeMessage = options.welcomeMessage; - - const addChat = new CommandToolbarButton({ - commands: this._commands, - id: CommandIDs.createChat, - args: { inSidePanel: true }, - icon: addIcon - }); - addChat.addClass(ADD_BUTTON_CLASS); - this.toolbar.addItem('createChat', addChat); - - this._openChat = ReactWidget.create( - - ); - - this._openChat.addClass(OPEN_SELECT_CLASS); - this.toolbar.addItem('openChat', this._openChat); - - const content = this.content as AccordionPanel; - content.expansionToggled.connect(this._onExpansionToggled, this); - - this._contentsManager.fileChanged.connect((_, args) => { - if (args.type === 'delete') { - this.widgets.forEach(widget => { - if ((widget as ChatSection).path === args.oldValue?.path) { - widget.dispose(); - } - }); - this.updateChatList(); - } - const updateActions = ['new', 'rename']; - if ( - updateActions.includes(args.type) && - args.newValue?.path?.endsWith(chatFileType.extensions[0]) - ) { - this.updateChatList(); - } - }); - } - - /** - * Getter and setter of the defaultDirectory. - */ - get defaultDirectory(): string { - return this._defaultDirectory; - } - set defaultDirectory(value: string) { - if (value === this._defaultDirectory) { - return; - } - this._defaultDirectory = value; - // Update the list of discoverable chat (in default directory) - this.updateChatList(); - // Update the sections names. - this.widgets.forEach(w => { - (w as ChatSection).defaultDirectory = value; - }); - } - - /** - * Add a new widget to the chat panel. - * - * @param model - the model of the chat widget - * @param name - the name of the chat. - */ - addChat(model: IChatModel): ChatWidget { - // Collapse all chats - const content = this.content as AccordionPanel; - for (let i = 0; i < this.widgets.length; i++) { - content.collapse(i); - } - - // Create the toolbar registry. - let inputToolbarRegistry: IInputToolbarRegistry | undefined; - if (this._inputToolbarFactory) { - inputToolbarRegistry = this._inputToolbarFactory.create(); - } - - // Create a new widget. - const widget = new ChatWidget({ - model: model, - rmRegistry: this._rmRegistry, - themeManager: this._themeManager, - chatCommandRegistry: this._chatCommandRegistry, - attachmentOpenerRegistry: this._attachmentOpenerRegistry, - inputToolbarRegistry, - messageFooterRegistry: this._messageFooterRegistry, - welcomeMessage: this._welcomeMessage - }); - - this.addWidget( - new ChatSection({ - widget, - commands: this._commands, - path: model.name, - defaultDirectory: this._defaultDirectory - }) - ); - - return widget; - } - - /** - * Update the list of available chats in the default directory. - */ - updateChatList = async (): Promise => { - const extension = chatFileType.extensions[0]; - this._contentsManager - .get(this._defaultDirectory) - .then((contentModel: Contents.IModel) => { - const chatsNames: { [name: string]: string } = {}; - (contentModel.content as any[]) - .filter(f => f.type === 'file' && f.name.endsWith(extension)) - .forEach(f => { - chatsNames[PathExt.basename(f.name, extension)] = f.path; - }); - - this._chatNamesChanged.emit(chatsNames); - }) - .catch(e => console.error('Error getting the chat files from drive', e)); - }; - - /** - * Open a chat if it exists in the side panel. - * - * @param path - the path of the chat. - * @returns a boolean, whether the chat existed in the side panel or not. - */ - openIfExists(path: string): boolean { - const index = this._getChatIndex(path); - if (index > -1) { - this._expandChat(index); - } - return index > -1; - } - - /** - * A message handler invoked on an `'after-attach'` message. - */ - protected onAfterAttach(msg: Message): void { - // Wait for the component to be rendered. - this._openChat.renderPromise?.then(() => this.updateChatList()); - } - - /** - * Return the index of the chat in the list (-1 if not opened). - * - * @param name - the chat name. - */ - private _getChatIndex(path: string) { - return this.widgets.findIndex(w => (w as ChatSection).path === path); - } - - /** - * Expand the chat from its index. - */ - private _expandChat(index: number): void { - if (!this.widgets[index].isVisible) { - (this.content as AccordionPanel).expand(index); - } - } - - /** - * Handle `change` events for the HTMLSelect component. - */ - private _chatSelected = ( - event: React.ChangeEvent - ): void => { - const select = event.target; - const path = select.value; - const name = select.options[select.selectedIndex].textContent; - if (name === '-') { - return; - } - - this._commands.execute(CommandIDs.openChat, { - filepath: path, - inSidePanel: true - }); - event.target.selectedIndex = 0; - }; - - /** - * Triggered when a section is toogled. If the section is opened, all others - * sections are closed. - */ - private _onExpansionToggled(panel: AccordionPanel, index: number) { - if (!this.widgets[index].isVisible) { - return; - } - for (let i = 0; i < this.widgets.length; i++) { - if (i !== index) { - panel.collapse(i); - } - } - } - - private _chatNamesChanged = new Signal( - this - ); - private _commands: CommandRegistry; - private _defaultDirectory: string; - private _contentsManager: Contents.IManager; - private _openChat: ReactWidget; - private _rmRegistry: IRenderMimeRegistry; - private _themeManager: IThemeManager | null; - private _chatCommandRegistry?: IChatCommandRegistry; - private _attachmentOpenerRegistry?: IAttachmentOpenerRegistry; - private _inputToolbarFactory?: IInputToolbarRegistryFactory; - private _messageFooterRegistry?: IMessageFooterRegistry; - private _welcomeMessage?: string; -} - -/** - * The chat panel namespace. - */ -export namespace ChatPanel { - /** - * Options of the constructor of the chat panel. - */ - export interface IOptions extends SidePanel.IOptions { - commands: CommandRegistry; - contentsManager: Contents.IManager; - rmRegistry: IRenderMimeRegistry; - themeManager: IThemeManager | null; - defaultDirectory: string; - chatCommandRegistry?: IChatCommandRegistry; - attachmentOpenerRegistry?: IAttachmentOpenerRegistry; - inputToolbarFactory?: IInputToolbarRegistryFactory; - messageFooterRegistry?: IMessageFooterRegistry; - welcomeMessage?: string; - } -} - -/** - * The chat section containing a chat widget. - */ -class ChatSection extends PanelWithToolbar { - /** - * Constructor of the chat section. - */ - constructor(options: ChatSection.IOptions) { - super(options); - - this.addWidget(options.widget); - this.addWidget(this._spinner); - - this.addClass(SECTION_CLASS); - this._defaultDirectory = options.defaultDirectory; - this._path = options.path; - this._updateTitle(); - this.toolbar.addClass(TOOLBAR_CLASS); - - this._markAsRead = new ToolbarButton({ - icon: readIcon, - iconLabel: 'Mark chat as read', - className: 'jp-mod-styled', - onClick: () => (this.model.unreadMessages = []) - }); - - const moveToMain = new ToolbarButton({ - icon: launchIcon, - iconLabel: 'Move the chat to the main area', - className: 'jp-mod-styled', - onClick: () => { - this.model.dispose(); - options.commands.execute(CommandIDs.openChat, { - filepath: this._path - }); - this.dispose(); - } - }); - - const closeButton = new ToolbarButton({ - icon: closeIcon, - iconLabel: 'Close the chat', - className: 'jp-mod-styled', - onClick: () => { - this.model.dispose(); - this.dispose(); - } - }); - - this.toolbar.addItem('jupyterlabChat-markRead', this._markAsRead); - this.toolbar.addItem('jupyterlabChat-moveMain', moveToMain); - this.toolbar.addItem('jupyterlabChat-close', closeButton); - - this.model.unreadChanged?.connect(this._unreadChanged); - - this._markAsRead.enabled = this.model.unreadMessages.length > 0; - - options.widget.node.style.height = '100%'; - - /** - * Remove the spinner when the chat is ready. - */ - const model = this.model as LabChatModel; - model.ready.then(() => { - this._spinner.dispose(); - }); - } - - /** - * The path of the chat. - */ - get path(): string { - return this._path; - } - - /** - * Set the default directory property. - */ - set defaultDirectory(value: string) { - this._defaultDirectory = value; - this._updateTitle(); - } - - /** - * The model of the widget. - */ - get model(): IChatModel { - return (this.widgets[0] as ChatWidget).model; - } - - /** - * Dispose of the resources held by the widget. - */ - dispose(): void { - this.model.unreadChanged?.disconnect(this._unreadChanged); - super.dispose(); - } - - /** - * Update the section's title, depending on the default directory and chat file name. - * If the chat file is in the default directory, the section's name is its relative - * path to that default directory. Otherwise, it is it absolute path. - */ - private _updateTitle(): void { - const inDefault = this._defaultDirectory - ? !PathExt.relative(this._defaultDirectory, this._path).startsWith('..') - : true; - - const pattern = new RegExp(`${chatFileType.extensions[0]}$`, 'g'); - this.title.label = ( - inDefault - ? this._defaultDirectory - ? PathExt.relative(this._defaultDirectory, this._path) - : this._path - : '/' + this._path - ).replace(pattern, ''); - this.title.caption = this._path; - } - - /** - * Change the title when messages are unread. - * - * TODO: fix it upstream in @jupyterlab/ui-components. - * Updating the title create a new Title widget, but does not attach again the - * toolbar. The toolbar is attached only when the title widget is attached the first - * time. - */ - private _unreadChanged = (_: IChatModel, unread: number[]) => { - this._markAsRead.enabled = unread.length > 0; - // this.title.label = `${unread.length ? '* ' : ''}${this._name}`; - }; - - private _defaultDirectory: string; - private _markAsRead: ToolbarButton; - private _path: string; - private _spinner = new Spinner(); -} - -/** - * The chat section namespace. - */ -export namespace ChatSection { - /** - * Options to build a chat section. - */ - export interface IOptions extends Panel.IOptions { - commands: CommandRegistry; - defaultDirectory: string; - widget: ChatWidget; - path: string; - } -} - -type ChatSelectProps = { - chatNamesChanged: ISignal; - handleChange: (event: React.ChangeEvent) => void; -}; - -/** - * A component to select a chat from the drive. - */ -function ChatSelect({ - chatNamesChanged, - handleChange -}: ChatSelectProps): JSX.Element { - // An object associating a chat name to its path. Both are purely indicative, the name - // is the section title and the path is used as caption. - const [chatNames, setChatNames] = useState<{ [name: string]: string }>({}); - - // Update the chat list. - chatNamesChanged.connect((_, chatNames) => { - setChatNames(chatNames); +export function createMultiChatPanel(options: { + commands: CommandRegistry; + contentsManager: Contents.IManager; + rmRegistry: IRenderMimeRegistry; + themeManager: IThemeManager | null; + defaultDirectory: string; + chatCommandRegistry?: IChatCommandRegistry; + attachmentOpenerRegistry?: IAttachmentOpenerRegistry; + inputToolbarFactory?: IInputToolbarRegistryFactory; + messageFooterRegistry?: IMessageFooterRegistry; + welcomeMessage?: string; +}): MultiChatPanel { + const panel = new MultiChatPanel({ + commands: options.commands, + contentsManager: options.contentsManager, + rmRegistry: options.rmRegistry, + themeManager: options.themeManager, + defaultDirectory: options.defaultDirectory, + chatFileExtension: chatFileType.extensions[0], + createChatCommand: CommandIDs.createChat, + openChatCommand: CommandIDs.openChat, + chatCommandRegistry: options.chatCommandRegistry, + attachmentOpenerRegistry: options.attachmentOpenerRegistry, + inputToolbarFactory: options.inputToolbarFactory, + messageFooterRegistry: options.messageFooterRegistry, + welcomeMessage: options.welcomeMessage }); - return ( - - - {Object.keys(chatNames).map(name => ( - - ))} - - ); + return panel; } From eb48176c396170f96ab9d47f34ea6758d680df64 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 8 Aug 2025 07:13:18 +0000 Subject: [PATCH 02/15] Automatic application of license header --- packages/jupyter-chat/src/multiChatPanel.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/jupyter-chat/src/multiChatPanel.tsx b/packages/jupyter-chat/src/multiChatPanel.tsx index 21523f61..b3e5c82f 100644 --- a/packages/jupyter-chat/src/multiChatPanel.tsx +++ b/packages/jupyter-chat/src/multiChatPanel.tsx @@ -1,3 +1,8 @@ +/* + * Copyright (c) Jupyter Development Team. + * Distributed under the terms of the Modified BSD License. + */ + /* * Multi-chat panel for @jupyter/chat * Originally adapted from jupyterlab-chat's ChatPanel From ffbe6b1f913022747ccd081c18592dcadeaf2a02 Mon Sep 17 00:00:00 2001 From: Nakul Date: Fri, 8 Aug 2025 13:13:34 +0530 Subject: [PATCH 03/15] Adding comments --- packages/jupyter-chat/src/multiChatPanel.tsx | 96 ++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/packages/jupyter-chat/src/multiChatPanel.tsx b/packages/jupyter-chat/src/multiChatPanel.tsx index b3e5c82f..b64dd563 100644 --- a/packages/jupyter-chat/src/multiChatPanel.tsx +++ b/packages/jupyter-chat/src/multiChatPanel.tsx @@ -48,6 +48,9 @@ const TOOLBAR_CLASS = 'jp-chat-toolbar'; * Generic sidepanel widget including multiple chats and the add chat button. */ export class MultiChatPanel extends SidePanel { + /** + * The constructor of the chat panel. + */ constructor(options: ChatPanel.IOptions) { super(options); this.addClass(SIDEPANEL_CLASS); @@ -105,6 +108,9 @@ export class MultiChatPanel extends SidePanel { }); } + /** + * Getter and setter of the defaultDirectory. + */ get defaultDirectory(): string { return this._defaultDirectory; } @@ -113,23 +119,34 @@ export class MultiChatPanel extends SidePanel { return; } this._defaultDirectory = value; + // Update the list of discoverable chat (in default directory) this.updateChatList(); + // Update the sections names. this.widgets.forEach(w => { (w as ChatSection).defaultDirectory = value; }); } + /** + * Add a new widget to the chat panel. + * + * @param model - the model of the chat widget + * @param name - the name of the chat. + */ + addChat(model: IChatModel): ChatWidget { const content = this.content as AccordionPanel; for (let i = 0; i < this.widgets.length; i++) { content.collapse(i); } + // Create the toolbar registry. let inputToolbarRegistry: IInputToolbarRegistry | undefined; if (this._inputToolbarFactory) { inputToolbarRegistry = this._inputToolbarFactory.create(); } + // Create a new widget. const widget = new ChatWidget({ model, rmRegistry: this._rmRegistry, @@ -154,6 +171,9 @@ export class MultiChatPanel extends SidePanel { return widget; } + /** + * Update the list of available chats in the default directory. + */ updateChatList = async (): Promise => { const extension = this._chatFileExtension; this._contentsManager @@ -170,6 +190,12 @@ export class MultiChatPanel extends SidePanel { .catch(e => console.error('Error getting chat files from drive', e)); }; + /** + * Open a chat if it exists in the side panel. + * + * @param path - the path of the chat. + * @returns a boolean, whether the chat existed in the side panel or not. + */ openIfExists(path: string): boolean { const index = this._getChatIndex(path); if (index > -1) { @@ -178,20 +204,34 @@ export class MultiChatPanel extends SidePanel { return index > -1; } + /** + * A message handler invoked on an `'after-attach'` message. + */ protected onAfterAttach(): void { this._openChat.renderPromise?.then(() => this.updateChatList()); } + /** + * Return the index of the chat in the list (-1 if not opened). + * + * @param name - the chat name. + */ private _getChatIndex(path: string) { return this.widgets.findIndex(w => (w as ChatSection).path === path); } + /** + * Expand the chat from its index. + */ private _expandChat(index: number): void { if (!this.widgets[index].isVisible) { (this.content as AccordionPanel).expand(index); } } + /** + * Handle `change` events for the HTMLSelect component. + */ private _chatSelected = ( event: React.ChangeEvent ): void => { @@ -208,6 +248,10 @@ export class MultiChatPanel extends SidePanel { event.target.selectedIndex = 0; }; + /** + * Triggered when a section is toogled. If the section is opened, all others + * sections are closed. + */ private _onExpansionToggled(panel: AccordionPanel, index: number) { if (!this.widgets[index].isVisible) { return; @@ -238,7 +282,13 @@ export class MultiChatPanel extends SidePanel { private _welcomeMessage?: string; } +/** + * The chat panel namespace. + */ export namespace ChatPanel { + /** + * Options of the constructor of the chat panel. + */ export interface IOptions extends SidePanel.IOptions { commands: CommandRegistry; contentsManager: Contents.IManager; @@ -259,7 +309,13 @@ export namespace ChatPanel { } } +/** + * The chat section containing a chat widget. + */ class ChatSection extends PanelWithToolbar { + /** + * Constructor of the chat section. + */ constructor(options: ChatSection.IOptions) { super(options); this.addWidget(options.widget); @@ -309,29 +365,49 @@ class ChatSection extends PanelWithToolbar { this._markAsRead.enabled = this.model.unreadMessages.length > 0; options.widget.node.style.height = '100%'; + /** + * Remove the spinner when the chat is ready. + */ (this.model as any).ready?.then?.(() => { this._spinner.dispose(); }); } + /** + * The path of the chat. + */ get path(): string { return this._path; } + /** + * Set the default directory property. + */ set defaultDirectory(value: string) { this._defaultDirectory = value; this._updateTitle(); } + /** + * The model of the widget. + */ get model(): IChatModel { return (this.widgets[0] as ChatWidget).model; } + /** + * Dispose of the resources held by the widget. + */ dispose(): void { this.model.unreadChanged?.disconnect(this._unreadChanged); super.dispose(); } + /** + * Update the section's title, depending on the default directory and chat file name. + * If the chat file is in the default directory, the section's name is its relative + * path to that default directory. Otherwise, it is it absolute path. + */ private _updateTitle(): void { const inDefault = this._defaultDirectory ? !PathExt.relative(this._defaultDirectory, this._path).startsWith('..') @@ -347,6 +423,14 @@ class ChatSection extends PanelWithToolbar { this.title.caption = this._path; } + /** + * Change the title when messages are unread. + * + * TODO: fix it upstream in @jupyterlab/ui-components. + * Updating the title create a new Title widget, but does not attach again the + * toolbar. The toolbar is attached only when the title widget is attached the first + * time. + */ private _unreadChanged = (_: IChatModel, unread: number[]) => { this._markAsRead.enabled = unread.length > 0; }; @@ -358,7 +442,13 @@ class ChatSection extends PanelWithToolbar { private _spinner = new Spinner(); } +/** + * The chat section namespace. + */ export namespace ChatSection { + /** + * Options to build a chat section. + */ export interface IOptions extends Panel.IOptions { commands: CommandRegistry; defaultDirectory: string; @@ -373,11 +463,17 @@ type ChatSelectProps = { handleChange: (event: React.ChangeEvent) => void; }; +/** + * A component to select a chat from the drive. + */ function ChatSelect({ chatNamesChanged, handleChange }: ChatSelectProps): JSX.Element { + // An object associating a chat name to its path. Both are purely indicative, the name + // is the section title and the path is used as caption. const [chatNames, setChatNames] = useState<{ [name: string]: string }>({}); + // Update the chat list. chatNamesChanged.connect((_, names) => setChatNames(names)); return ( From fb036f3c5216d5bfcc1a32aedff2f5aeaad281c8 Mon Sep 17 00:00:00 2001 From: Nakul Date: Tue, 12 Aug 2025 17:22:08 +0530 Subject: [PATCH 04/15] adding rename-chat button and ... --- packages/jupyter-chat/src/index.ts | 1 + packages/jupyter-chat/src/model.ts | 22 ++ packages/jupyter-chat/src/multiChatPanel.tsx | 271 +++++++++++------- packages/jupyter-chat/src/token.ts | 26 ++ .../jupyter-chat/src/utils/renameDialog.ts | 66 +++++ packages/jupyter-chat/style/input.css | 73 +++++ .../jupyterlab-chat-extension/src/index.ts | 69 ++++- packages/jupyterlab-chat/src/factory.ts | 9 +- packages/jupyterlab-chat/src/model.ts | 11 +- packages/jupyterlab-chat/src/token.ts | 31 +- packages/jupyterlab-chat/src/widget.tsx | 76 ++++- 11 files changed, 493 insertions(+), 162 deletions(-) create mode 100644 packages/jupyter-chat/src/token.ts create mode 100644 packages/jupyter-chat/src/utils/renameDialog.ts diff --git a/packages/jupyter-chat/src/index.ts b/packages/jupyter-chat/src/index.ts index aef58aa6..1b5f971f 100644 --- a/packages/jupyter-chat/src/index.ts +++ b/packages/jupyter-chat/src/index.ts @@ -14,3 +14,4 @@ export * from './selection-watcher'; export * from './types'; export * from './widgets'; export * from './multiChatPanel'; +export * from './token'; diff --git a/packages/jupyter-chat/src/model.ts b/packages/jupyter-chat/src/model.ts index eb0aa9d6..f75f2943 100644 --- a/packages/jupyter-chat/src/model.ts +++ b/packages/jupyter-chat/src/model.ts @@ -20,6 +20,7 @@ import { IUser } from './types'; import { replaceMentionToSpan } from './utils'; +import { PromiseDelegate } from '@lumino/coreutils'; /** * The chat model interface. @@ -40,6 +41,11 @@ export interface IChatModel extends IDisposable { */ unreadMessages: number[]; + /** + * The promise resolving when the model is ready. + */ + readonly ready: Promise; + /** * The indexes list of the messages currently in the viewport. */ @@ -241,6 +247,9 @@ export abstract class AbstractChatModel implements IChatModel { this._activeCellManager = options.activeCellManager ?? null; this._selectionWatcher = options.selectionWatcher ?? null; this._documentManager = options.documentManager ?? null; + + this._readyDelegate = new PromiseDelegate(); + this.ready = this._readyDelegate.promise; } /** @@ -328,6 +337,18 @@ export abstract class AbstractChatModel implements IChatModel { localStorage.setItem(`@jupyter/chat:${this._id}`, JSON.stringify(storage)); } + /** + * Promise that resolves when the model is ready. + */ + readonly ready: Promise; + + /** + * Mark the model as ready. + */ + protected markReady(): void { + this._readyDelegate.resolve(); + } + /** * The chat settings. */ @@ -677,6 +698,7 @@ export abstract class AbstractChatModel implements IChatModel { private _id: string | undefined; private _name: string = ''; private _config: IConfig; + private _readyDelegate: PromiseDelegate; private _inputModel: IInputModel; private _isDisposed = false; private _commands?: CommandRegistry; diff --git a/packages/jupyter-chat/src/multiChatPanel.tsx b/packages/jupyter-chat/src/multiChatPanel.tsx index b64dd563..262013dc 100644 --- a/packages/jupyter-chat/src/multiChatPanel.tsx +++ b/packages/jupyter-chat/src/multiChatPanel.tsx @@ -17,14 +17,13 @@ import { IMessageFooterRegistry, readIcon } from './index'; -import { Contents } from '@jupyterlab/services'; import { IThemeManager } from '@jupyterlab/apputils'; import { PathExt } from '@jupyterlab/coreutils'; +import { ContentsManager } from '@jupyterlab/services'; import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; import { addIcon, closeIcon, - CommandToolbarButton, HTMLSelect, launchIcon, PanelWithToolbar, @@ -33,10 +32,10 @@ import { Spinner, ToolbarButton } from '@jupyterlab/ui-components'; -import { CommandRegistry } from '@lumino/commands'; import { ISignal, Signal } from '@lumino/signaling'; import { AccordionPanel, Panel } from '@lumino/widgets'; import React, { useState } from 'react'; +import { showRenameDialog } from './utils/renameDialog'; const SIDEPANEL_CLASS = 'jp-chat-sidepanel'; const ADD_BUTTON_CLASS = 'jp-chat-add'; @@ -48,64 +47,55 @@ const TOOLBAR_CLASS = 'jp-chat-toolbar'; * Generic sidepanel widget including multiple chats and the add chat button. */ export class MultiChatPanel extends SidePanel { - /** - * The constructor of the chat panel. - */ constructor(options: ChatPanel.IOptions) { super(options); this.addClass(SIDEPANEL_CLASS); - this._commands = options.commands; - this._contentsManager = options.contentsManager; + + this._defaultDirectory = options.defaultDirectory; this._rmRegistry = options.rmRegistry; this._themeManager = options.themeManager; - this._defaultDirectory = options.defaultDirectory; - this._chatFileExtension = options.chatFileExtension; - this._createChatCommand = options.createChatCommand; - this._openChatCommand = options.openChatCommand; this._chatCommandRegistry = options.chatCommandRegistry; this._attachmentOpenerRegistry = options.attachmentOpenerRegistry; this._inputToolbarFactory = options.inputToolbarFactory; this._messageFooterRegistry = options.messageFooterRegistry; this._welcomeMessage = options.welcomeMessage; - - const addChat = new CommandToolbarButton({ - commands: this._commands, - id: this._createChatCommand, - args: { inSidePanel: true }, - icon: addIcon + this._getChatNames = options.getChatNames; + this._onChatsChanged = options.onChatsChanged; + + // Use the passed callback functions + this._openChat = options.openChat ?? (() => {}); + this._createChat = options.createChat ?? (() => {}); + this._closeChat = options.closeChat ?? (() => {}); + this._moveToMain = options.moveToMain ?? (() => {}); + + // Add chat button calls the createChat callback + const addChat = new ToolbarButton({ + onClick: () => this._createChat(), + icon: addIcon, + label: 'Chat', + tooltip: 'Add a new chat' }); addChat.addClass(ADD_BUTTON_CLASS); this.toolbar.addItem('createChat', addChat); - this._openChat = ReactWidget.create( + // Chat select dropdown + this._openChatWidget = ReactWidget.create( ); - this._openChat.addClass(OPEN_SELECT_CLASS); - this.toolbar.addItem('openChat', this._openChat); + this._openChatWidget.addClass(OPEN_SELECT_CLASS); + this.toolbar.addItem('openChat', this._openChatWidget); const content = this.content as AccordionPanel; content.expansionToggled.connect(this._onExpansionToggled, this); - this._contentsManager.fileChanged.connect((_, args) => { - if (args.type === 'delete') { - this.widgets.forEach(widget => { - if ((widget as ChatSection).path === args.oldValue?.path) { - widget.dispose(); - } - }); - this.updateChatList(); - } - const updateActions = ['new', 'rename']; - if ( - updateActions.includes(args.type) && - args.newValue?.path?.endsWith(this._chatFileExtension) - ) { + if (this._onChatsChanged) { + this._onChatsChanged(() => { this.updateChatList(); - } - }); + }); + } } /** @@ -158,15 +148,18 @@ export class MultiChatPanel extends SidePanel { welcomeMessage: this._welcomeMessage }); - this.addWidget( - new ChatSection({ - widget, - commands: this._commands, - path: model.name, - defaultDirectory: this._defaultDirectory, - openChatCommand: this._openChatCommand - }) - ); + const section = new ChatSection({ + widget, + path: model.name, + defaultDirectory: this._defaultDirectory, + openChat: this._openChat, + closeChat: this._closeChat, + moveToMain: this._moveToMain, + renameChat: this._renameChat + }); + + this.addWidget(section); + content.expand(this.widgets.length - 1); return widget; } @@ -175,19 +168,12 @@ export class MultiChatPanel extends SidePanel { * Update the list of available chats in the default directory. */ updateChatList = async (): Promise => { - const extension = this._chatFileExtension; - this._contentsManager - .get(this._defaultDirectory) - .then((contentModel: Contents.IModel) => { - const chatsNames: { [name: string]: string } = {}; - (contentModel.content as any[]) - .filter(f => f.type === 'file' && f.name.endsWith(extension)) - .forEach(f => { - chatsNames[PathExt.basename(f.name, extension)] = f.path; - }); - this._chatNamesChanged.emit(chatsNames); - }) - .catch(e => console.error('Error getting chat files from drive', e)); + try { + const chatsNames = await this._getChatNames(); + this._chatNamesChanged.emit(chatsNames); + } catch (e) { + console.error('Error getting chat files', e); + } }; /** @@ -208,7 +194,7 @@ export class MultiChatPanel extends SidePanel { * A message handler invoked on an `'after-attach'` message. */ protected onAfterAttach(): void { - this._openChat.renderPromise?.then(() => this.updateChatList()); + this._openChatWidget.renderPromise?.then(() => this.updateChatList()); } /** @@ -232,20 +218,44 @@ export class MultiChatPanel extends SidePanel { /** * Handle `change` events for the HTMLSelect component. */ - private _chatSelected = ( - event: React.ChangeEvent - ): void => { - const select = event.target; - const path = select.value; - const name = select.options[select.selectedIndex].textContent; - if (name === '-') { + private _chatSelected(event: React.ChangeEvent): void { + const path = event.target.value; + if (path === '-') { return; } - this._commands.execute(this._openChatCommand, { - filepath: path, - inSidePanel: true - }); + this._openChat(path); event.target.selectedIndex = 0; + } + + /** + * Rename a chat. + */ + private _renameChat = async ( + section: ChatSection, + path: string, + newName: string + ) => { + try { + const oldPath = path; + const newPath = PathExt.join(this.defaultDirectory, newName); + + const ext = '.chat'; + if (!newName.endsWith(ext)) { + newName += ext; + } + + const contentsManager = new ContentsManager(); + await contentsManager.rename(oldPath, newPath); + + // Now update UI after backend rename + section.updateDisplayName(newName); + section.updatePath(newPath); + this.updateChatList(); + + console.log(`Renamed chat ${oldPath} to ${newPath}`); + } catch (e) { + console.error('Error renaming chat', e); + } }; /** @@ -266,13 +276,8 @@ export class MultiChatPanel extends SidePanel { private _chatNamesChanged = new Signal( this ); - private _commands: CommandRegistry; + private _defaultDirectory: string; - private _chatFileExtension: string; - private _createChatCommand: string; - private _openChatCommand: string; - private _contentsManager: Contents.IManager; - private _openChat: ReactWidget; private _rmRegistry: IRenderMimeRegistry; private _themeManager: IThemeManager | null; private _chatCommandRegistry?: IChatCommandRegistry; @@ -280,6 +285,16 @@ export class MultiChatPanel extends SidePanel { private _inputToolbarFactory?: ChatPanel.IInputToolbarRegistryFactory; private _messageFooterRegistry?: IMessageFooterRegistry; private _welcomeMessage?: string; + private _getChatNames: () => Promise<{ [name: string]: string }>; + + // Replaced command strings with callback functions: + private _openChat: (path: string) => void; + private _createChat: () => void; + private _closeChat: (path: string) => void; + private _moveToMain: (path: string) => void; + + private _onChatsChanged?: (cb: () => void) => void; + private _openChatWidget: ReactWidget; } /** @@ -290,20 +305,31 @@ export namespace ChatPanel { * Options of the constructor of the chat panel. */ export interface IOptions extends SidePanel.IOptions { - commands: CommandRegistry; - contentsManager: Contents.IManager; rmRegistry: IRenderMimeRegistry; themeManager: IThemeManager | null; defaultDirectory: string; chatFileExtension: string; - createChatCommand: string; - openChatCommand: string; + getChatNames: () => Promise<{ [name: string]: string }>; + onChatsChanged?: (cb: () => void) => void; + + // Callback functions instead of command strings + openChat: (path: string) => void; + createChat: () => void; + closeChat: (path: string) => void; + moveToMain: (path: string) => void; + renameChat: ( + section: ChatSection.IOptions, + path: string, + newName: string + ) => void; + chatCommandRegistry?: IChatCommandRegistry; attachmentOpenerRegistry?: IAttachmentOpenerRegistry; inputToolbarFactory?: IInputToolbarRegistryFactory; messageFooterRegistry?: IMessageFooterRegistry; welcomeMessage?: string; } + export interface IInputToolbarRegistryFactory { create(): IInputToolbarRegistry; } @@ -323,9 +349,11 @@ class ChatSection extends PanelWithToolbar { this.addClass(SECTION_CLASS); this._defaultDirectory = options.defaultDirectory; this._path = options.path; - this._openChatCommand = options.openChatCommand; - this._updateTitle(); + this._closeChat = options.closeChat; + this._renameChat = options.renameChat; this.toolbar.addClass(TOOLBAR_CLASS); + this._displayName = PathExt.basename(this._path); + this._updateTitle(); this._markAsRead = new ToolbarButton({ icon: readIcon, @@ -334,15 +362,25 @@ class ChatSection extends PanelWithToolbar { onClick: () => (this.model.unreadMessages = []) }); + const renameButton = new ToolbarButton({ + iconClass: 'jp-EditIcon', + iconLabel: 'Rename chat', + className: 'jp-mod-styled', + onClick: async () => { + const newName = await showRenameDialog(this.title.label); + if (newName && newName.trim() && newName !== this.title.label) { + this._renameChat(this, this._path, newName.trim()); + } + } + }); + const moveToMain = new ToolbarButton({ icon: launchIcon, iconLabel: 'Move the chat to the main area', className: 'jp-mod-styled', onClick: () => { - this.model.dispose(); - options.commands.execute(this._openChatCommand, { - filepath: this._path - }); + options.openChat(this._path); + options.renameChat(this, this._path, this._displayName); this.dispose(); } }); @@ -353,22 +391,29 @@ class ChatSection extends PanelWithToolbar { className: 'jp-mod-styled', onClick: () => { this.model.dispose(); + this._closeChat(this._path); this.dispose(); } }); this.toolbar.addItem('markRead', this._markAsRead); + this.toolbar.addItem('rename', renameButton); this.toolbar.addItem('moveMain', moveToMain); this.toolbar.addItem('close', closeButton); + this.toolbar.node.style.backgroundColor = 'js-toolbar-background'; + this.toolbar.node.style.minHeight = '32px'; + this.toolbar.node.style.display = 'flex'; + this.model.unreadChanged?.connect(this._unreadChanged); this._markAsRead.enabled = this.model.unreadMessages.length > 0; options.widget.node.style.height = '100%'; + /** * Remove the spinner when the chat is ready. */ - (this.model as any).ready?.then?.(() => { + this.model.ready.then(() => { this._spinner.dispose(); }); } @@ -380,6 +425,13 @@ class ChatSection extends PanelWithToolbar { return this._path; } + /** + * The default directory of the chat. + */ + get defaultDirectory(): string { + return this._defaultDirectory; + } + /** * Set the default directory property. */ @@ -409,20 +461,22 @@ class ChatSection extends PanelWithToolbar { * path to that default directory. Otherwise, it is it absolute path. */ private _updateTitle(): void { - const inDefault = this._defaultDirectory - ? !PathExt.relative(this._defaultDirectory, this._path).startsWith('..') - : true; - const pattern = new RegExp(`${this._path.split('.').pop()}$`, 'g'); - this.title.label = ( - inDefault - ? this._defaultDirectory - ? PathExt.relative(this._defaultDirectory, this._path) - : this._path - : '/' + this._path - ).replace(pattern, ''); + console.log('Updating title label:', this._displayName); + this.title.label = this._displayName; this.title.caption = this._path; } + public updateDisplayName(newName: string) { + this._path = PathExt.join(this.defaultDirectory, newName); + this._displayName = newName; + this._updateTitle(); + } + + public updatePath(newPath: string) { + this._path = newPath; + this._updateTitle(); + } + /** * Change the title when messages are unread. * @@ -436,10 +490,17 @@ class ChatSection extends PanelWithToolbar { }; private _defaultDirectory: string; - private _markAsRead: ToolbarButton; private _path: string; - private _openChatCommand: string; + private _markAsRead: ToolbarButton; private _spinner = new Spinner(); + private _displayName: string; + + private _closeChat: (path: string) => void; + private _renameChat: ( + section: ChatSection, + path: string, + newName: string + ) => void; } /** @@ -450,11 +511,13 @@ export namespace ChatSection { * Options to build a chat section. */ export interface IOptions extends Panel.IOptions { - commands: CommandRegistry; - defaultDirectory: string; widget: ChatWidget; path: string; - openChatCommand: string; + defaultDirectory: string; + openChat: (path: string) => void; + closeChat: (path: string) => void; + moveToMain: (path: string) => void; + renameChat: (section: ChatSection, path: string, newName: string) => void; } } diff --git a/packages/jupyter-chat/src/token.ts b/packages/jupyter-chat/src/token.ts new file mode 100644 index 00000000..abc7171d --- /dev/null +++ b/packages/jupyter-chat/src/token.ts @@ -0,0 +1,26 @@ +/* + * Copyright (c) Jupyter Development Team. + * Distributed under the terms of the Modified BSD License. + */ + +import { Token } from '@lumino/coreutils'; +import { IInputToolbarRegistry } from './index'; + +/** + * A factory interface for creating a new Input Toolbar Registry + * for each Chat Panel. + */ +export interface IInputToolbarRegistryFactory { + /** + * Create a new input toolbar registry instance. + */ + create: () => IInputToolbarRegistry; +} + +/** + * The token of the factory to create an input toolbar registry. + */ +export const IInputToolbarRegistryFactory = + new Token( + '@jupyter/chat:IInputToolbarRegistryFactory' + ); diff --git a/packages/jupyter-chat/src/utils/renameDialog.ts b/packages/jupyter-chat/src/utils/renameDialog.ts new file mode 100644 index 00000000..e30752bc --- /dev/null +++ b/packages/jupyter-chat/src/utils/renameDialog.ts @@ -0,0 +1,66 @@ +/* + * Copyright (c) Jupyter Development Team. + * Distributed under the terms of the Modified BSD License. + */ +import '../../style/input.css'; + +export async function showRenameDialog( + currentName: string +): Promise { + return new Promise(resolve => { + const modal = document.createElement('div'); + modal.className = 'rename-modal'; + + const dialog = document.createElement('div'); + dialog.className = 'rename-dialog'; + modal.appendChild(dialog); + + const title = document.createElement('h3'); + title.textContent = 'Rename Chat'; + dialog.appendChild(title); + + const input = document.createElement('input'); + input.type = 'text'; + input.value = currentName; + dialog.appendChild(input); + + const buttons = document.createElement('div'); + buttons.className = 'rename-buttons'; + + const cancelBtn = document.createElement('button'); + cancelBtn.textContent = 'Cancel'; + cancelBtn.className = 'cancel-btn'; + cancelBtn.onclick = () => { + document.body.removeChild(modal); + resolve(null); + }; + buttons.appendChild(cancelBtn); + + const okBtn = document.createElement('button'); + okBtn.textContent = 'Rename'; + okBtn.className = 'rename-ok'; + okBtn.onclick = () => { + const val = input.value.trim(); + if (val) { + document.body.removeChild(modal); + resolve(val); + } else { + input.focus(); + } + }; + buttons.appendChild(okBtn); + + dialog.appendChild(buttons); + + document.body.appendChild(modal); + input.focus(); + + input.addEventListener('keydown', e => { + if (e.key === 'Enter') { + okBtn.click(); + } else if (e.key === 'Escape') { + cancelBtn.click(); + } + }); + }); +} diff --git a/packages/jupyter-chat/style/input.css b/packages/jupyter-chat/style/input.css index f9d4009b..ac83bcf8 100644 --- a/packages/jupyter-chat/style/input.css +++ b/packages/jupyter-chat/style/input.css @@ -72,3 +72,76 @@ border-radius: 3px; white-space: nowrap; } + +.rename-modal { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: rgba(41, 41, 41, 0.4); + display: flex; + justify-content: center; + align-items: center; + z-index: 1000; +} + +.rename-dialog { + background: var(--jp-layout-color2); + padding: 1rem 1.5rem; + border-radius: 8px; + min-width: 300px; + box-shadow: 0 4px 8px rgba(24, 23, 23, 0.26); + color: var(--jp-ui-font-color1); + border: 1px solid var(--jp-border-color0); +} + +.rename-dialog h3 { + margin-top: 0; + color: var(--jp-ui-font-color1); +} + +.rename-dialog input[type="text"] { + width: 100%; + padding: 0.4rem 0.6rem; + margin-bottom: 1rem; + font-size: 1rem; + border: 1px solid var(--jp-border-color1); + border-radius: 3px; + background-color: var(--jp-layout-color1); + color: var(--jp-ui-font-color1); +} + +.rename-dialog input[type="text"]::placeholder { + color: var(--jp-ui-font-color2); +} + +.rename-buttons { + display: flex; + justify-content: flex-end; + gap: 0.5rem; +} + +.rename-buttons button { + cursor: pointer; + padding: 0.3rem 0.7rem; + border-radius: 3px; + border: 1px solid var(--jp-border-color2); + background-color: var(--jp-layout-color1); + color: var(--jp-ui-font-color1); + font-size: 0.9rem; + transition: background-color 0.2s ease; +} + +.rename-buttons button.cancel-btn { + background-color: var(--jp-layout-color1); + border-color: var(--jp-border-color2); + color: var(--jp-ui-font-color1); +} + +.rename-buttons button.rename-ok { + font-weight: bold; + background-color: var(--jp-brand-color1); + border-color: var(--jp-brand-color1); + color: var(--jp-ui-font-color1); +} diff --git a/packages/jupyterlab-chat-extension/src/index.ts b/packages/jupyterlab-chat-extension/src/index.ts index 3bbe3a2a..1796592b 100644 --- a/packages/jupyterlab-chat-extension/src/index.ts +++ b/packages/jupyterlab-chat-extension/src/index.ts @@ -18,7 +18,8 @@ import { MessageFooterRegistry, SelectionWatcher, chatIcon, - readIcon + readIcon, + IInputToolbarRegistryFactory } from '@jupyter/chat'; import { ICollaborativeContentProvider } from '@jupyter/collaborative-drive'; import { @@ -54,7 +55,6 @@ import { IActiveCellManagerToken, IChatFactory, IChatPanel, - IInputToolbarRegistryFactory, ISelectionWatcherToken, IWelcomeMessage, LabChatModel, @@ -64,7 +64,7 @@ import { YChat, chatFileType } from 'jupyterlab-chat'; -import { MultiChatPanel as ChatPanel } from '@jupyter/chat'; +import { MultiChatPanel as ChatPanel, ChatSection } from '@jupyter/chat'; import { chatCommandRegistryPlugin } from './chat-commands/plugins'; import { emojiCommandsPlugin } from './chat-commands/providers/emoji'; import { mentionCommandsPlugin } from './chat-commands/providers/user-mention'; @@ -723,17 +723,45 @@ const chatPanel: JupyterFrontEndPlugin = { themeManager: IThemeManager | null, welcomeMessage: string ): ChatPanel => { - const { commands } = app; + const { commands, serviceManager } = app; const defaultDirectory = factory.widgetConfig.config.defaultDirectory || ''; + const chatFileExtension = chatFileType.extensions[0]; + + const getChatNames = async () => { + const dirContents = await serviceManager.contents.get(defaultDirectory); + const names: { [name: string]: string } = {}; + for (const file of dirContents.content) { + if (file.type === 'file' && file.name.endsWith(chatFileExtension)) { + const nameWithoutExt = file.name.replace(chatFileExtension, ''); + names[nameWithoutExt] = file.path; + } + } + return names; + }; + + // Hook that fires when files change + const onChatsChanged = (cb: () => void) => { + serviceManager.contents.fileChanged.connect( + (_sender: any, change: { type: string }) => { + if ( + change.type === 'new' || + change.type === 'delete' || + change.type === 'rename' + ) { + cb(); + } + } + ); + }; /** * Add Chat widget to left sidebar */ const chatPanel = new ChatPanel({ - commands, - contentsManager: app.serviceManager.contents, rmRegistry, + getChatNames, + onChatsChanged, themeManager, defaultDirectory, chatCommandRegistry, @@ -741,9 +769,32 @@ const chatPanel: JupyterFrontEndPlugin = { inputToolbarFactory, messageFooterRegistry, welcomeMessage, - chatFileExtension: chatFileType.extensions[0], - createChatCommand: CommandIDs.createChat, - openChatCommand: CommandIDs.openChat + chatFileExtension, + createChat: () => { + commands.execute(CommandIDs.createChat); + }, + openChat: (path: string) => { + commands.execute(CommandIDs.openChat, { filepath: path }); + }, + closeChat: (path: string) => { + commands.execute(CommandIDs.closeChat, { filepath: path }); + }, + moveToMain: (path: string) => { + commands.execute(CommandIDs.moveToMain, { filepath: path }); + }, + renameChat: ( + section: ChatSection.IOptions, + path: string, + newName: string + ) => { + if (section.widget.title.label !== newName) { + const newPath = `${defaultDirectory}/${newName}${chatFileExtension}`; + serviceManager.contents + .rename(path, newPath) + .catch(err => console.error('Rename failed:', err)); + section.widget.title.label = newName; + } + } }); chatPanel.id = 'JupyterlabChat:sidepanel'; chatPanel.title.icon = chatIcon; diff --git a/packages/jupyterlab-chat/src/factory.ts b/packages/jupyterlab-chat/src/factory.ts index 3242817b..2badb370 100644 --- a/packages/jupyterlab-chat/src/factory.ts +++ b/packages/jupyterlab-chat/src/factory.ts @@ -10,7 +10,8 @@ import { IChatCommandRegistry, IInputToolbarRegistry, IMessageFooterRegistry, - ISelectionWatcher + ISelectionWatcher, + IInputToolbarRegistryFactory } from '@jupyter/chat'; import { IThemeManager } from '@jupyterlab/apputils'; import { IDocumentManager } from '@jupyterlab/docmanager'; @@ -23,11 +24,7 @@ import { ISignal, Signal } from '@lumino/signaling'; import { LabChatModel } from './model'; import { LabChatPanel } from './widget'; import { YChat } from './ychat'; -import { - IInputToolbarRegistryFactory, - ILabChatConfig, - IWidgetConfig -} from './token'; +import { ILabChatConfig, IWidgetConfig } from './token'; /** * The object provided by the chatDocument extension. diff --git a/packages/jupyterlab-chat/src/model.ts b/packages/jupyterlab-chat/src/model.ts index 4939450c..fad02926 100644 --- a/packages/jupyterlab-chat/src/model.ts +++ b/packages/jupyterlab-chat/src/model.ts @@ -17,7 +17,7 @@ import { import { IChangedArgs } from '@jupyterlab/coreutils'; import { DocumentRegistry } from '@jupyterlab/docregistry'; import { User } from '@jupyterlab/services'; -import { PartialJSONObject, PromiseDelegate, UUID } from '@lumino/coreutils'; +import { PartialJSONObject, UUID } from '@lumino/coreutils'; import { ISignal, Signal } from '@lumino/signaling'; import { IWidgetConfig } from './token'; @@ -137,10 +137,6 @@ export class LabChatModel return this._stateChanged; } - get ready(): Promise { - return this._ready.promise; - } - get dirty(): boolean { return this._dirty; } @@ -162,7 +158,7 @@ export class LabChatModel set id(value: string | undefined) { super.id = value; if (value) { - this._ready.resolve(); + this.markReady(); } } @@ -202,7 +198,7 @@ export class LabChatModel ): Promise { // Ensure the chat has an ID before inserting the messages, to properly catch the // unread messages (the last read message is saved using the chat ID). - return this._ready.promise.then(() => { + return this.ready.then(() => { super.messagesInserted(index, messages); }); } @@ -489,7 +485,6 @@ export class LabChatModel readonly defaultKernelName: string = ''; readonly defaultKernelLanguage: string = ''; - private _ready = new PromiseDelegate(); private _sharedModel: YChat; private _dirty = false; diff --git a/packages/jupyterlab-chat/src/token.ts b/packages/jupyterlab-chat/src/token.ts index 0dd3668d..4100014c 100644 --- a/packages/jupyterlab-chat/src/token.ts +++ b/packages/jupyterlab-chat/src/token.ts @@ -8,8 +8,7 @@ import { chatIcon, IActiveCellManager, ISelectionWatcher, - ChatWidget, - IInputToolbarRegistry + ChatWidget } from '@jupyter/chat'; import { WidgetTracker } from '@jupyterlab/apputils'; import { DocumentRegistry } from '@jupyterlab/docregistry'; @@ -106,7 +105,15 @@ export const CommandIDs = { /** * Focus the input of the current chat. */ - focusInput: 'jupyterlab-chat:focusInput' + focusInput: 'jupyterlab-chat:focusInput', + /** + * Close the current chat. + */ + closeChat: 'jupyterlab-chat:closeChat', + /** + * Move a main widget to the main area. + */ + moveToMain: 'jupyterlab-chat:moveToMain' }; /** @@ -128,24 +135,6 @@ export const ISelectionWatcherToken = new Token( 'jupyterlab-chat:ISelectionWatcher' ); -/** - * The input toolbar registry factory. - */ -export interface IInputToolbarRegistryFactory { - /** - * Create an input toolbar registry. - */ - create: () => IInputToolbarRegistry; -} - -/** - * The token of the factory to create an input toolbar registry. - */ -export const IInputToolbarRegistryFactory = - new Token( - 'jupyterlab-chat:IInputToolbarRegistryFactory' - ); - /** * The token to add a welcome message to the chat. * This token is not provided by default, but can be provided by third party extensions diff --git a/packages/jupyterlab-chat/src/widget.tsx b/packages/jupyterlab-chat/src/widget.tsx index 13e24361..0d291374 100644 --- a/packages/jupyterlab-chat/src/widget.tsx +++ b/packages/jupyterlab-chat/src/widget.tsx @@ -8,9 +8,10 @@ import { IAttachmentOpenerRegistry, IChatCommandRegistry, IChatModel, - IMessageFooterRegistry + IMessageFooterRegistry, + IInputToolbarRegistryFactory } from '@jupyter/chat'; -import { MultiChatPanel } from '@jupyter/chat'; +import { MultiChatPanel, ChatSection } from '@jupyter/chat'; import { Contents } from '@jupyterlab/services'; import { IThemeManager } from '@jupyterlab/apputils'; import { DocumentWidget } from '@jupyterlab/docregistry'; @@ -18,11 +19,7 @@ import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; import { CommandRegistry } from '@lumino/commands'; import { LabChatModel } from './model'; -import { - CommandIDs, - IInputToolbarRegistryFactory, - chatFileType -} from './token'; +import { CommandIDs, chatFileType } from './token'; const MAIN_PANEL_CLASS = 'jp-lab-chat-main-panel'; const TITLE_UNREAD_CLASS = 'jp-lab-chat-title-unread'; @@ -84,21 +81,72 @@ export function createMultiChatPanel(options: { messageFooterRegistry?: IMessageFooterRegistry; welcomeMessage?: string; }): MultiChatPanel { - const panel = new MultiChatPanel({ - commands: options.commands, - contentsManager: options.contentsManager, + const { contentsManager, defaultDirectory } = options; + const chatFileExtension = chatFileType.extensions[0]; + + // This function replaces updateChatList's file lookup + const getChatNames = async () => { + const dirContents = await contentsManager.get(defaultDirectory); + const names: { [name: string]: string } = {}; + for (const file of dirContents.content) { + if (file.type === 'file' && file.name.endsWith(chatFileExtension)) { + const nameWithoutExt = file.name.replace(chatFileExtension, ''); + names[nameWithoutExt] = file.path; + } + } + return names; + }; + + // Hook that fires when files change + const onChatsChanged = (cb: () => void) => { + contentsManager.fileChanged.connect((_sender, change) => { + if ( + change.type === 'new' || + change.type === 'delete' || + (change.type === 'rename' && + change.oldValue?.path !== change.newValue?.path) + ) { + cb(); + } + }); + }; + + return new MultiChatPanel({ rmRegistry: options.rmRegistry, themeManager: options.themeManager, defaultDirectory: options.defaultDirectory, chatFileExtension: chatFileType.extensions[0], - createChatCommand: CommandIDs.createChat, - openChatCommand: CommandIDs.openChat, + getChatNames, + onChatsChanged, + createChat: () => { + options.commands.execute(CommandIDs.createChat); + }, + openChat: path => { + options.commands.execute(CommandIDs.openChat, { filepath: path }); + }, + closeChat: path => { + options.commands.execute(CommandIDs.closeChat, { filepath: path }); + }, + moveToMain: path => { + options.commands.execute(CommandIDs.moveToMain, { filepath: path }); + }, + renameChat: ( + section: ChatSection.IOptions, + path: string, + newName: string + ) => { + if (section.widget.title.label !== newName) { + const newPath = `${defaultDirectory}/${newName}${chatFileExtension}`; + contentsManager + .rename(path, newPath) + .catch(err => console.error('Rename failed:', err)); + section.widget.title.label = newName; + } + }, chatCommandRegistry: options.chatCommandRegistry, attachmentOpenerRegistry: options.attachmentOpenerRegistry, inputToolbarFactory: options.inputToolbarFactory, messageFooterRegistry: options.messageFooterRegistry, welcomeMessage: options.welcomeMessage }); - - return panel; } From 26e3fd7101223ac56fc58cb23edaa9d129ac2167 Mon Sep 17 00:00:00 2001 From: Nakul Date: Wed, 13 Aug 2025 10:39:34 +0530 Subject: [PATCH 05/15] lint fix --- packages/jupyter-chat/style/input.css | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/jupyter-chat/style/input.css b/packages/jupyter-chat/style/input.css index ac83bcf8..56abb8eb 100644 --- a/packages/jupyter-chat/style/input.css +++ b/packages/jupyter-chat/style/input.css @@ -79,7 +79,7 @@ left: 0; width: 100vw; height: 100vh; - background-color: rgba(41, 41, 41, 0.4); + background-color: rgb(41 41 41 / 40%); display: flex; justify-content: center; align-items: center; @@ -91,7 +91,7 @@ padding: 1rem 1.5rem; border-radius: 8px; min-width: 300px; - box-shadow: 0 4px 8px rgba(24, 23, 23, 0.26); + box-shadow: 0 4px 8px rgb(24 23 23 / 26%); color: var(--jp-ui-font-color1); border: 1px solid var(--jp-border-color0); } @@ -101,7 +101,7 @@ color: var(--jp-ui-font-color1); } -.rename-dialog input[type="text"] { +.rename-dialog input[type='text'] { width: 100%; padding: 0.4rem 0.6rem; margin-bottom: 1rem; @@ -112,7 +112,7 @@ color: var(--jp-ui-font-color1); } -.rename-dialog input[type="text"]::placeholder { +.rename-dialog input[type='text']::placeholder { color: var(--jp-ui-font-color2); } @@ -122,6 +122,7 @@ gap: 0.5rem; } +/* stylelint-disable-next-line no-descending-specificity */ .rename-buttons button { cursor: pointer; padding: 0.3rem 0.7rem; From 7118e78123c260cc501131211af7dcd9002ee872 Mon Sep 17 00:00:00 2001 From: Nakul Date: Fri, 15 Aug 2025 17:19:30 +0530 Subject: [PATCH 06/15] Updating --- packages/jupyter-chat/src/multiChatPanel.tsx | 36 ++++++++++++-------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/packages/jupyter-chat/src/multiChatPanel.tsx b/packages/jupyter-chat/src/multiChatPanel.tsx index 262013dc..33b8c700 100644 --- a/packages/jupyter-chat/src/multiChatPanel.tsx +++ b/packages/jupyter-chat/src/multiChatPanel.tsx @@ -33,7 +33,7 @@ import { ToolbarButton } from '@jupyterlab/ui-components'; import { ISignal, Signal } from '@lumino/signaling'; -import { AccordionPanel, Panel } from '@lumino/widgets'; +import { AccordionPanel, Panel, Widget } from '@lumino/widgets'; import React, { useState } from 'react'; import { showRenameDialog } from './utils/renameDialog'; @@ -169,8 +169,9 @@ export class MultiChatPanel extends SidePanel { */ updateChatList = async (): Promise => { try { - const chatsNames = await this._getChatNames(); - this._chatNamesChanged.emit(chatsNames); + const chatNames = await this._getChatNames(); + console.log('updateChatList emits:', chatNames); + this._chatNamesChanged.emit(chatNames); } catch (e) { console.error('Error getting chat files', e); } @@ -352,7 +353,7 @@ class ChatSection extends PanelWithToolbar { this._closeChat = options.closeChat; this._renameChat = options.renameChat; this.toolbar.addClass(TOOLBAR_CLASS); - this._displayName = PathExt.basename(this._path); + this._displayName = this._path.replace(/\.chat$/, ''); this._updateTitle(); this._markAsRead = new ToolbarButton({ @@ -379,9 +380,13 @@ class ChatSection extends PanelWithToolbar { iconLabel: 'Move the chat to the main area', className: 'jp-mod-styled', onClick: () => { - options.openChat(this._path); - options.renameChat(this, this._path, this._displayName); - this.dispose(); + const mainWidget = options.openChat(this._path) as Widget | undefined; + + if (mainWidget) { + mainWidget.disposed.connect(() => { + this.dispose(); + }); + } } }); @@ -461,13 +466,12 @@ class ChatSection extends PanelWithToolbar { * path to that default directory. Otherwise, it is it absolute path. */ private _updateTitle(): void { - console.log('Updating title label:', this._displayName); this.title.label = this._displayName; this.title.caption = this._path; } public updateDisplayName(newName: string) { - this._path = PathExt.join(this.defaultDirectory, newName); + this._path = PathExt.join(this.defaultDirectory, `${newName}.chat`); this._displayName = newName; this._updateTitle(); } @@ -536,16 +540,18 @@ function ChatSelect({ // An object associating a chat name to its path. Both are purely indicative, the name // is the section title and the path is used as caption. const [chatNames, setChatNames] = useState<{ [name: string]: string }>({}); + // Update the chat list. - chatNamesChanged.connect((_, names) => setChatNames(names)); + chatNamesChanged.connect((_, chatNames) => { + setChatNames(chatNames); + }); + return ( - - + {Object.keys(chatNames).map(name => ( - + ))} + ); } From 1273e272643fd0151e1afad433ce66c900820574 Mon Sep 17 00:00:00 2001 From: Nakul Date: Fri, 15 Aug 2025 17:22:42 +0530 Subject: [PATCH 07/15] ui tests --- ui-tests/tests/commands.spec.ts | 2 +- .../launcher-tile-linux.png | Bin 1441 -> 1385 bytes .../menu-new-linux.png | Bin 7390 -> 7513 bytes ui-tests/tests/notebook-application.spec.ts | 4 +- .../tab-with-unread-linux.png | Bin 1164 -> 1686 bytes .../tab-without-unread-linux.png | Bin 1533 -> 1575 bytes ui-tests/tests/side-panel.spec.ts | 57 ++++++++++++------ .../moveToMain-linux.png | Bin 225 -> 226 bytes ui-tests/tests/test-utils.ts | 4 +- .../not-stacked-messages-linux.png | Bin 5117 -> 5540 bytes .../stacked-messages-linux.png | Bin 3605 -> 3709 bytes .../navigation-bottom-linux.png | Bin 451 -> 519 bytes .../navigation-bottom-unread-linux.png | Bin 956 -> 1108 bytes .../navigation-top-linux.png | Bin 1188 -> 1197 bytes 14 files changed, 45 insertions(+), 22 deletions(-) diff --git a/ui-tests/tests/commands.spec.ts b/ui-tests/tests/commands.spec.ts index 7622f254..08c6a6e2 100644 --- a/ui-tests/tests/commands.spec.ts +++ b/ui-tests/tests/commands.spec.ts @@ -49,7 +49,7 @@ test.describe('#commandPalette', () => { .click(); await fillModal(page, name); await page.waitForCondition( - async () => await page.filebrowser.contents.fileExists(FILENAME) + async () => await page.filebrowser.contents.fileExists(`tests-commands--commandPal-bb324-h-name-from-command-palette/${FILENAME}`) ); await expect(page.activity.getTabLocator(FILENAME)).toBeVisible(); }); diff --git a/ui-tests/tests/commands.spec.ts-snapshots/launcher-tile-linux.png b/ui-tests/tests/commands.spec.ts-snapshots/launcher-tile-linux.png index ca959328bb3806ebbee6ed889f8296dbcdd42da7..8d197e0150d3141333a721edb5cd13ac876f0f12 100644 GIT binary patch literal 1385 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 diff --git a/ui-tests/tests/commands.spec.ts-snapshots/menu-new-linux.png b/ui-tests/tests/commands.spec.ts-snapshots/menu-new-linux.png index 543b35216ced7293a213529137102309ed057106..94e94962051a36605e40884cf68eb80acc490107 100644 GIT binary patch literal 7513 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 vTsI#VJ%X??a58UlY#q@4uZ#iNxD&JTTZ5`e literal 7390 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 JLUC=c{{U~Qt`PtL diff --git a/ui-tests/tests/notebook-application.spec.ts b/ui-tests/tests/notebook-application.spec.ts index 06907024..444b4f35 100644 --- a/ui-tests/tests/notebook-application.spec.ts +++ b/ui-tests/tests/notebook-application.spec.ts @@ -40,10 +40,10 @@ test.describe('#NotebookApp', () => { await page.menu.clickMenuItem('View>Left Sidebar>Show Jupyter Chat'); const panel = page.locator('#jp-left-stack'); await expect(panel).toBeVisible(); - await expect(panel.locator('.jp-lab-chat-sidepanel')).toBeVisible(); + await expect(panel.locator('.jp-chat-sidepanel')).toBeVisible(); const select = panel.locator( - '.jp-SidePanel-toolbar .jp-Toolbar-item.jp-lab-chat-open select' + '.jp-SidePanel-toolbar .jp-Toolbar-item.jp-chat-open select' ); await expect(select.locator('option')).toHaveCount(2); diff --git a/ui-tests/tests/notifications.spec.ts-snapshots/tab-with-unread-linux.png b/ui-tests/tests/notifications.spec.ts-snapshots/tab-with-unread-linux.png index 9984cb5711faa8056d8bf7269a0e55abb01bbc13..5b9a62f9d86e9cfdcdf1a183ad9dcf6246400779 100644 GIT binary patch delta 1657 zcmV-<28Q{J36>3zFn5L=2)%R^);=%#ZhaLo zgHSvEfex~v)6>qW&aHtSoSWIx=C(Nq4rIZgm5jmE@sK8n(0Gsz9_--7Ic)|MGxT8J zq44%FY1JleqF4>hd``*lz4x8p%zXL1Kg>|2Qh}J5pUtlT@PEhC-+p!)F$2xN$#=i} z>>OfZpj8q7`|J0gokq++v$V7nSHzgUAU0x5Ul1EHrZ0$%7}FR0x_fi6SbX#5O?&*r zi4!+&+}N|MVg`;b74hZEmy#q+e#x0LXI{K`(M6a!I##XVd*<2&-%_zr+XG={Ygj9U zd!W2;SX*0j9Dk=h?l{ic+FCCPT`D2~96NSQlBAAJj^i9XdUSMj^vf_~fDjTzQ4mD{ zU?D>S>Lr;Ui|5pkOcUQ<*snQ^%AFtCKIQf6*Z1$=|M>Ca)vH&xf)5`)ESJlIAP_>j zDs-ucoO90kPOXcJiw_<=*cN2AjwPaKS(o_*U8NYSIe)K*d-=hzTF$iny-;?oOu-$n z`bh{$rBaU{KelZplwImQ>&f!P{0EZfv% zf-zByg6lc0KeCo{831#1qI_~Mb9Ag^+sy3QrX~|f!bH~XXbpgvXQfq{5KLq(Q*XI} zxj9{x>3?TYR?lc@Cv?)3QrWC;6ZV~qrVuPim}*;D1dgf6lweFX)7e~ccm+dKC?%Ls zHHx}9%_#Wwj6$hw*n?!p@$vCgDpe|#%H{IH!ouR>Vk(syA0O{^e5F$1J2O8&Un-S~ zUvlTpo!Qx0&iU-@?EL(EhhcB;PM@2;w_3e9mVdmpvc9cxy*NEGlDt)Ia?aP6ZzV@Y z#%?}oaL)PaKU1Sqci(c(m*>Wkw^yS6&Ej+``ngDTZfxw&d#mf5^XA*eI_LcD-Kmk0 z$=fSU&N*)s|2#K#tEJFbSy^4@ob$%L>CxohtDJM*xH~m^v)Uo|mD|a4)BmhAIp^z* zMt^fF;kjyqbIzO9xv{N*&DE9FCg+?tpWGZfH@D0==j%_VN2l*KcX_D$nWs;m&dkir z%*-q;E%iRCQmJ&Gnv2EayLazC4<0#kWO8zHmr;xfST_`j8it$z)}e=_s8j#|L}Uqx zvL=fF01_c$;D-P}R2iH?9RMJ7Y!@rawtux;-F5`UP$dKaLPFeJf)Z870sw$WsTBHt zbZrr132jEB3+#w%J4`kr6(ownX2P;YMF0SS$`}Gan$!geOF|T-2^IvMZgsFYaNxkV z4eYX685$b8aN)wmix=O&f4{BC^Sq6XjgF&`q(rlELNTM}94vwdw%_02X+FceP6pgd!HU zM^Uuz+9Hw&>sBUL^BDtx&|{*qoA3Zotyc5-{N&^$=RBX!NAq=$W4jpf?Afyu6BCaf zJt~z-9XlL8eE91V?uCS^N^+s*5`V`dIz<3DUGvhLtt>zg1_3~uW!mtvhT+PVmBk3a z&ne2M#bzf*iXsYvp4N6j!O(5VvIu$$c2|KU5aMQjg5`3@?sYw zUcP)808XDi-QM)!!-wb3pMPuG-4SGsWOJDykWJe5Y7tSWnaSETt4Ih!KY!>@9$T<5 z5JZd-05I3Cc@T9J1O$QW0>uCTK`oPUiD^&)D>CTWoRZTqg3u4pj>2IOg4mjxeaChg zC=gY93id?^beQ0PXpU)pWc(9j^bN6#Z02m$~9v&WU zj~_pN{MoZ-U!QhwAgWPQt$)jD+@VpBGr7>r=p-F5l#nx4R^CAdYbH&bvLXlwVO5p| zH&T}k-A?PIfQad26JEVu7XqLHVAD#QW}4>w5JY0+ENzFQMoCMnhN&o)fRLmpiqxuz zL^E*Opae-q!BTg!@&4k}sZ&!^Q^{oV^We2>*A5*zbn@iMUKEBZm2%4Y^XI=ggIBL! zb*z`)16DI=Rn48PhB~6W8I1u#Z*EgZ9Q%sDyl*uaHcOx;F)du}2WYFl#iWoC^#72zi3t}V2^acL|fqPCbZf#;200000NkvXXu0mjf DDBmb{ delta 1131 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 { const items = toolbar.locator('.jp-Toolbar-item'); await expect(items).toHaveCount(2); - await expect(items.first()).toHaveClass(/.jp-lab-chat-add/); - await expect(items.last()).toHaveClass(/.jp-lab-chat-open/); + await expect(items.first()).toHaveClass(/.jp-chat-add/); + await expect(items.last()).toHaveClass(/.jp-chat-open/); }); test('chat panel should not contain a chat at init', async ({ page }) => { @@ -50,7 +50,7 @@ test.describe('#sidepanel', () => { test.beforeEach(async ({ page }) => { panel = await openSidePanel(page); addButton = panel.locator( - '.jp-SidePanel-toolbar .jp-Toolbar-item.jp-lab-chat-add' + '.jp-SidePanel-toolbar .jp-Toolbar-item.jp-chat-add' ); await addButton.click(); @@ -73,6 +73,11 @@ test.describe('#sidepanel', () => { async () => await page.filebrowser.contents.fileExists(FILENAME) ); + + const chatPanel = await openChat(page, FILENAME); + const button = chatPanel.getByTitle('Move the chat to the side panel'); + await button.click(); + const chatTitle = panel.locator( '.jp-SidePanel-content .jp-AccordionPanel-title' ); @@ -91,6 +96,10 @@ test.describe('#sidepanel', () => { async () => await page.filebrowser.contents.fileExists('untitled.chat') ); + const chatPanel = await openChat(page, FILENAME); + const button = chatPanel.getByTitle('Move the chat to the side panel'); + await button.click(); + const chatTitle = panel.locator( '.jp-SidePanel-content .jp-AccordionPanel-title' ); @@ -116,6 +125,10 @@ test.describe('#sidepanel', () => { async () => await page.filebrowser.contents.fileExists(FILENAME) ); + const chatPanel = await openChat(page, FILENAME); + const button = chatPanel.getByTitle('Move the chat to the side panel'); + await button.click(); + const chatTitle = panel.locator( '.jp-SidePanel-content .jp-AccordionPanel-title' ); @@ -167,7 +180,7 @@ test.describe('#sidepanel', () => { test('should list existing chat', async ({ page }) => { panel = await openSidePanel(page); select = panel.locator( - '.jp-SidePanel-toolbar .jp-Toolbar-item.jp-lab-chat-open select' + '.jp-SidePanel-toolbar .jp-Toolbar-item.jp-chat-open select' ); await expect(select.locator('option')).toHaveCount(2); @@ -177,7 +190,7 @@ test.describe('#sidepanel', () => { test('should attach a spinner while loading the chat', async ({ page }) => { panel = await openSidePanel(page); select = panel.locator( - '.jp-SidePanel-toolbar .jp-Toolbar-item.jp-lab-chat-open select' + '.jp-SidePanel-toolbar .jp-Toolbar-item.jp-chat-open select' ); await select.selectOption(name); await expect(panel.locator('.jp-Spinner')).toBeAttached(); @@ -187,11 +200,15 @@ test.describe('#sidepanel', () => { test('should open an existing chat and close it', async ({ page }) => { panel = await openSidePanel(page); select = panel.locator( - '.jp-SidePanel-toolbar .jp-Toolbar-item.jp-lab-chat-open select' + '.jp-SidePanel-toolbar .jp-Toolbar-item.jp-chat-open select' ); await select.selectOption(name); + const chatPanel = await openChat(page, FILENAME); + const button = chatPanel.getByTitle('Move the chat to the side panel'); + await button.click(); + const chatTitle = panel.locator( '.jp-SidePanel-content .jp-AccordionPanel-title' ); @@ -207,7 +224,7 @@ test.describe('#sidepanel', () => { test('should list existing chat in default directory', async ({ page }) => { panel = await openSidePanel(page); select = panel.locator( - '.jp-SidePanel-toolbar .jp-Toolbar-item.jp-lab-chat-open select' + '.jp-SidePanel-toolbar .jp-Toolbar-item.jp-chat-open select' ); // changing the default directory to an empty one should empty the list. @@ -227,12 +244,12 @@ test.describe('#sidepanel', () => { /jp-mod-dirty/ ); - await expect(select.locator('option')).toHaveCount(1); + await expect(select.locator('option')).toHaveCount(2); await expect(select.locator('option').last()).toHaveText('Open a chat'); // creating a chat should populate the list. const addButton = panel.locator( - '.jp-SidePanel-toolbar .jp-Toolbar-item.jp-lab-chat-add' + '.jp-SidePanel-toolbar .jp-Toolbar-item.jp-chat-add' ); await addButton.click(); const dialog = page.locator('.jp-Dialog'); @@ -241,11 +258,17 @@ test.describe('#sidepanel', () => { await dialog.getByRole('button').getByText('Ok').click(); await expect(select.locator('option')).toHaveCount(2); - await expect(select.locator('option').last()).toHaveText('new-chat'); + await expect(select.locator('option', { hasText: 'new-chat' })).toHaveCount(1); - // Changing the default directory (to root) should update the chat list. - await defaultDirectory.clear(); + // Refresh the locator in case the old one is stale + const settings2 = await openSettings(page); + const defaultDirectory2 = settings2.locator( + 'input[label="defaultDirectory"]' + ); + // Changing the default directory (to root) should update the chat list. + await defaultDirectory2.clear(); + // wait for the settings to be saved await expect(page.activity.getTabLocator('Settings')).toHaveAttribute( 'class', @@ -256,8 +279,8 @@ test.describe('#sidepanel', () => { /jp-mod-dirty/ ); - await expect(select.locator('option')).toHaveCount(2); - await expect(select.locator('option').last()).toHaveText(name); + await expect(select.locator('option')).toHaveCount(3); + await expect(select.locator('option').nth(1)).toHaveText(name); }); }); @@ -288,7 +311,7 @@ test.describe('#sidepanel', () => { await button.click(); await expect(chatPanel).not.toBeAttached(); - const sidePanel = page.locator('.jp-SidePanel.jp-lab-chat-sidepanel'); + const sidePanel = page.locator('.jp-SidePanel.jp-chat-sidepanel'); await expect(sidePanel).toBeVisible(); const chatTitle = sidePanel.locator( '.jp-SidePanel-content .jp-AccordionPanel-title' @@ -307,7 +330,7 @@ test.describe('#sidepanel', () => { await button.click(); await expect(chatPanel).not.toBeAttached(); - const sidePanel = page.locator('.jp-SidePanel.jp-lab-chat-sidepanel'); + const sidePanel = page.locator('.jp-SidePanel.jp-chat-sidepanel'); await expect(sidePanel).toBeVisible(); const chatTitle = sidePanel.locator( '.jp-SidePanel-content .jp-AccordionPanel-title' @@ -339,7 +362,7 @@ test.describe('#sidepanel', () => { await expect(chatPanel).not.toBeAttached(); // Move the chat to the side panel. - const sidePanel = page.locator('.jp-SidePanel.jp-lab-chat-sidepanel'); + const sidePanel = page.locator('.jp-SidePanel.jp-chat-sidepanel'); await expect(sidePanel).toBeVisible(); const chatTitle = sidePanel.locator( '.jp-SidePanel-content .jp-AccordionPanel-title' diff --git a/ui-tests/tests/side-panel.spec.ts-snapshots/moveToMain-linux.png b/ui-tests/tests/side-panel.spec.ts-snapshots/moveToMain-linux.png index de8b4b1b1c470ce20a3d077f35f2bd07454f17c6..a218eb269b149a67ac2fd67c581c6064f82dd566 100644 GIT binary patch delta 211 zcmV;^04)FE0pbCW7k?HA0ssI2py5Do00001b5ch_0Itp)=>Px#oJmAMR5*?0lraiJ zAqi2gwKhB>qHcV)c5TXyaKmR{g`6spZoH}dar+NSY N002ovPDHLkV1l$~TxtLS delta 210 zcmV;@04@LG0pS6V7k?E90ssI2CUr8w00001b5ch_0Itp)=>Px#n@L1LR5*?0R6z;@ zAq?!6{zdUudQ|isp2OnNUrB<#!?Qi~P-}O=We;2E90DOTm`Q?5YwdOd_cP)j&_5=5 zN~u1LF>dR<_c`Zp>MNE~0HD_TF1JreDccSZQEQEeJ!jTG5laDpwU%>!*ndhXgy5VT z#Iyac>q => { - const panel = page.locator('.jp-SidePanel.jp-lab-chat-sidepanel'); + const panel = page.locator('.jp-SidePanel.jp-chat-sidepanel'); await page.evaluate(async filepath => { const inSidePanel = true; await window.jupyterapp.commands.execute('jupyterlab-chat:open', { @@ -175,7 +175,7 @@ export const openSettings = async ( export const openSidePanel = async ( page: IJupyterLabPageFixture ): Promise => { - const panel = page.locator('.jp-SidePanel.jp-lab-chat-sidepanel'); + const panel = page.locator('.jp-SidePanel.jp-chat-sidepanel'); if (!(await panel?.isVisible())) { const chatIcon = page.locator('.jp-SideBar').getByTitle('Jupyter Chat'); diff --git a/ui-tests/tests/ui-config.spec.ts-snapshots/not-stacked-messages-linux.png b/ui-tests/tests/ui-config.spec.ts-snapshots/not-stacked-messages-linux.png index 377dea0059a47be03c21f04fbe3f61fdebc43aaf..298fe5fa0cc1de8e0b1e1c2c8fb1d0eaeb3cb323 100644 GIT binary patch literal 5540 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 Kg1~FrKl~RleJmXS literal 5117 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 diff --git a/ui-tests/tests/unread.spec.ts-snapshots/navigation-bottom-linux.png b/ui-tests/tests/unread.spec.ts-snapshots/navigation-bottom-linux.png index 1f6338414904282e9d5d2da88d5594d47c382777..6da28745f989209a558e368adf19bcbb6c2f0b7f 100644 GIT binary patch delta 504 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 TcVGIc00000NkvXXu0mjfq0BY+ diff --git a/ui-tests/tests/unread.spec.ts-snapshots/navigation-top-linux.png b/ui-tests/tests/unread.spec.ts-snapshots/navigation-top-linux.png index 423311d95b44a42ea62238e983860685b277dea8..d21ca2bef1ee31ded6d0053b1c039d3eaaacb53c 100644 GIT binary patch delta 1164 zcmV;71ateO39Si`F@I-CL_t(YiKUibOj}hP$G`W|-nRFaUP`&Z1_R`80nGqohH-9e zkgx|6iAaMaWR>LZfwv2L1`&1 z>mOFQ?aJ-#y*{)cf4XkW@8h|j^T|0U=lp(`kYyPF+=CadhJTwrJd*AH^U}}m+SaCG zrF?5HaAU@QXMXv?vfvL2k|;7-I$uxi&DHHMHB{y4_~G7x?ABvOST9{1e3(|()>@^u zUG1_dy1KdmK%HHCtSK8|>%hRQTRqZgo)GthL19x!VI{n@61Lixzr8WfrBcUtr4gAj zpfOgZ4Opv;>VG&##8J2L%90uDZ=;yL|bwUVpErX}YJUXK``yab)cI`FJ7jdL&2* zx#{U?v)ODg7)&OU$z;-KG(9~%KA$fk9EnN*Bv~d6S_*(aByNXXE|=MC<~Xjsy`5!Q zmSsCSIy4$hcXxLv6xz=FgCc+oEtT?evuSa8wJMj%OIK}yv|4R68a;aSD8s~ykV>Vh zseh><2%@a4Y`b*K85)`n6mbk$v0Dq^mYv@Y5d_iL*htg##zC5<>+9fiw!E;o=h`D`IaJC;@xF81kjbB=dD6bdpl)p;yQ5@>hT>J6WzQ$4H+uTm#UYqY!-()xd&+wlf%O(dSw!i!wLe<*iZxq9pf^ zy1yQrixsfPg-fO0tIL1AI$J?*t;7kWadlK4wFgF~{CA!4-yQ(z43(*)UdYkyFHNt> eXSeHO`4>IF<2O3f&QSmW002ovP6b4+LSTXvPcx-F@Ii3L_t(YiKUi%Oj}hP$G`VdZrj_(Z7CNJ7*yU54G*K6ac*TL zVKGJ%LD7HIe~e2C8lzjXxW6=+iP_^HgkVgF7&I;tOiT?d`q(QHDG^81wt*IiK^%IXCD0ewUD>6?^}j@pQEg2!A^I?Fy60r~n|urJ?!I zt(m~xh1L73d>|Z+izKU|GPG1xuJ$={V!h%eb2foFn@wRdWd-N#!K`YdV$o)cnq+_4 z?7C``4#1-AXFJM__CGw*%>}W4E+rV@u_K}<81)3BcIWE1&}jd<6>2DWMW2|`8LM?$ z93e^4_H|QB;eWF~jynVE0FE$vK$r6o;zl;U04f_INL6J&+(O7OOGbE7? zP6a;vehk2|Cp8xujwq?*y6p+`;%C1)|8{Z!uDn@U_kX~!lSD$+*<5teq=N!>fv&)gbkwpMxcq86sodfXhvCNc%KV$O9Qiyks zW&&ut{(nz2CX(03m)D|#o|YXi&_1RT0AMO+G>Vj!NX$CnBkj&r0CmL-LHr*l2m+@{ zbO7wm5IMUX0dTUI*?*1QZg;s{`!gqsv;bz8BczYx0TiYy_Mt~dM|*mDIy*Z(9?w2l z5vu^;<9IR<<^k+;)g2B;cXzj5ucv9cqoZSKX@BWqWa9bxIeyc%&WlO8>FMdNt}cVY zV6j*%7K>7;?C9w5`~6AbSX=}kN)l;MQvd=HVK3zMdb_&1Se9*RX<-V_#-Y3KxK}GG*_?yMi#ag{3(_MjPm*X% zykuPf@IigSW|gWhPcL9;Z-~ESbw8f;;(u>0A19P#suFdYOg!6*jb)UKuucZs2ffl^ zwkt_!8}QzoU87{g=dYC|2LEd(wn_g-KimUw)Tp^|I+vzW4m56WCs*I9G?ngbC!1b} z|9tO6G$yj@)Q`t2?(iLVIMJf7G*=d8?6}Ljr2qs`>Kk7C+BTmkU=Itc zq24#;zgm+eBX?KgB+^ZFTpD(UZcGKn+=<^F9zdFk%Ft3z=V*_Yq}Ap#dv%HY3k?|M U9_#AW%>V!Z07*qoM6N<$g4aPc_y7O^ From b1ca944902fcd104b9633755d5b279e0d11caaac Mon Sep 17 00:00:00 2001 From: Nakul Date: Thu, 4 Sep 2025 09:00:13 +0530 Subject: [PATCH 08/15] removing content manager from @jupyter/chat package --- packages/jupyter-chat/src/index.ts | 2 +- packages/jupyter-chat/src/model.ts | 7 +- packages/jupyter-chat/src/widgets/index.ts | 1 + .../src/{ => widgets}/multiChatPanel.tsx | 213 ++++++------------ .../jupyterlab-chat-extension/src/index.ts | 107 +++++++-- packages/jupyterlab-chat/src/token.ts | 6 +- packages/jupyterlab-chat/src/widget.tsx | 20 +- 7 files changed, 171 insertions(+), 185 deletions(-) rename packages/jupyter-chat/src/{ => widgets}/multiChatPanel.tsx (72%) diff --git a/packages/jupyter-chat/src/index.ts b/packages/jupyter-chat/src/index.ts index 1b5f971f..61cbbfab 100644 --- a/packages/jupyter-chat/src/index.ts +++ b/packages/jupyter-chat/src/index.ts @@ -13,5 +13,5 @@ export * from './registers'; export * from './selection-watcher'; export * from './types'; export * from './widgets'; -export * from './multiChatPanel'; +export * from './widgets/multiChatPanel'; export * from './token'; diff --git a/packages/jupyter-chat/src/model.ts b/packages/jupyter-chat/src/model.ts index f75f2943..e0f31ac9 100644 --- a/packages/jupyter-chat/src/model.ts +++ b/packages/jupyter-chat/src/model.ts @@ -249,7 +249,6 @@ export abstract class AbstractChatModel implements IChatModel { this._documentManager = options.documentManager ?? null; this._readyDelegate = new PromiseDelegate(); - this.ready = this._readyDelegate.promise; } /** @@ -340,7 +339,9 @@ export abstract class AbstractChatModel implements IChatModel { /** * Promise that resolves when the model is ready. */ - readonly ready: Promise; + get ready(): Promise { + return this._readyDelegate.promise; + } /** * Mark the model as ready. @@ -698,7 +699,7 @@ export abstract class AbstractChatModel implements IChatModel { private _id: string | undefined; private _name: string = ''; private _config: IConfig; - private _readyDelegate: PromiseDelegate; + private _readyDelegate = new PromiseDelegate(); private _inputModel: IInputModel; private _isDisposed = false; private _commands?: CommandRegistry; diff --git a/packages/jupyter-chat/src/widgets/index.ts b/packages/jupyter-chat/src/widgets/index.ts index d2696485..d196e43b 100644 --- a/packages/jupyter-chat/src/widgets/index.ts +++ b/packages/jupyter-chat/src/widgets/index.ts @@ -6,3 +6,4 @@ export * from './chat-error'; export * from './chat-sidebar'; export * from './chat-widget'; +export * from './multiChatPanel'; diff --git a/packages/jupyter-chat/src/multiChatPanel.tsx b/packages/jupyter-chat/src/widgets/multiChatPanel.tsx similarity index 72% rename from packages/jupyter-chat/src/multiChatPanel.tsx rename to packages/jupyter-chat/src/widgets/multiChatPanel.tsx index 33b8c700..5cbdcdbd 100644 --- a/packages/jupyter-chat/src/multiChatPanel.tsx +++ b/packages/jupyter-chat/src/widgets/multiChatPanel.tsx @@ -16,10 +16,8 @@ import { IInputToolbarRegistry, IMessageFooterRegistry, readIcon -} from './index'; +} from '../index'; import { IThemeManager } from '@jupyterlab/apputils'; -import { PathExt } from '@jupyterlab/coreutils'; -import { ContentsManager } from '@jupyterlab/services'; import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; import { addIcon, @@ -33,9 +31,9 @@ import { ToolbarButton } from '@jupyterlab/ui-components'; import { ISignal, Signal } from '@lumino/signaling'; +import { showRenameDialog } from '../utils/renameDialog'; import { AccordionPanel, Panel, Widget } from '@lumino/widgets'; import React, { useState } from 'react'; -import { showRenameDialog } from './utils/renameDialog'; const SIDEPANEL_CLASS = 'jp-chat-sidepanel'; const ADD_BUTTON_CLASS = 'jp-chat-add'; @@ -51,7 +49,6 @@ export class MultiChatPanel extends SidePanel { super(options); this.addClass(SIDEPANEL_CLASS); - this._defaultDirectory = options.defaultDirectory; this._rmRegistry = options.rmRegistry; this._themeManager = options.themeManager; this._chatCommandRegistry = options.chatCommandRegistry; @@ -66,7 +63,7 @@ export class MultiChatPanel extends SidePanel { this._openChat = options.openChat ?? (() => {}); this._createChat = options.createChat ?? (() => {}); this._closeChat = options.closeChat ?? (() => {}); - this._moveToMain = options.moveToMain ?? (() => {}); + this._renameChatCallback = options.renameChat ?? (() => {}); // Add chat button calls the createChat callback const addChat = new ToolbarButton({ @@ -98,25 +95,6 @@ export class MultiChatPanel extends SidePanel { } } - /** - * Getter and setter of the defaultDirectory. - */ - get defaultDirectory(): string { - return this._defaultDirectory; - } - set defaultDirectory(value: string) { - if (value === this._defaultDirectory) { - return; - } - this._defaultDirectory = value; - // Update the list of discoverable chat (in default directory) - this.updateChatList(); - // Update the sections names. - this.widgets.forEach(w => { - (w as ChatSection).defaultDirectory = value; - }); - } - /** * Add a new widget to the chat panel. * @@ -150,11 +128,8 @@ export class MultiChatPanel extends SidePanel { const section = new ChatSection({ widget, - path: model.name, - defaultDirectory: this._defaultDirectory, openChat: this._openChat, closeChat: this._closeChat, - moveToMain: this._moveToMain, renameChat: this._renameChat }); @@ -183,8 +158,8 @@ export class MultiChatPanel extends SidePanel { * @param path - the path of the chat. * @returns a boolean, whether the chat existed in the side panel or not. */ - openIfExists(path: string): boolean { - const index = this._getChatIndex(path); + openIfExists(name: string): boolean { + const index = this._getChatIndex(name); if (index > -1) { this._expandChat(index); } @@ -198,13 +173,26 @@ export class MultiChatPanel extends SidePanel { this._openChatWidget.renderPromise?.then(() => this.updateChatList()); } + /** + * Rename a chat. + */ + private _renameChat = async (oldName: string, newName: string) => { + try { + await this._renameChatCallback?.(oldName, newName); + this.updateChatList(); + console.log(`Renamed chat ${oldName} → ${newName}`); + } catch (e) { + console.error('Error renaming chat', e); + } + }; + /** * Return the index of the chat in the list (-1 if not opened). * * @param name - the chat name. */ - private _getChatIndex(path: string) { - return this.widgets.findIndex(w => (w as ChatSection).path === path); + private _getChatIndex(name: string) { + return this.widgets.findIndex(w => (w as ChatSection).model?.name === name); } /** @@ -228,37 +216,6 @@ export class MultiChatPanel extends SidePanel { event.target.selectedIndex = 0; } - /** - * Rename a chat. - */ - private _renameChat = async ( - section: ChatSection, - path: string, - newName: string - ) => { - try { - const oldPath = path; - const newPath = PathExt.join(this.defaultDirectory, newName); - - const ext = '.chat'; - if (!newName.endsWith(ext)) { - newName += ext; - } - - const contentsManager = new ContentsManager(); - await contentsManager.rename(oldPath, newPath); - - // Now update UI after backend rename - section.updateDisplayName(newName); - section.updatePath(newPath); - this.updateChatList(); - - console.log(`Renamed chat ${oldPath} to ${newPath}`); - } catch (e) { - console.error('Error renaming chat', e); - } - }; - /** * Triggered when a section is toogled. If the section is opened, all others * sections are closed. @@ -278,7 +235,6 @@ export class MultiChatPanel extends SidePanel { this ); - private _defaultDirectory: string; private _rmRegistry: IRenderMimeRegistry; private _themeManager: IThemeManager | null; private _chatCommandRegistry?: IChatCommandRegistry; @@ -289,10 +245,13 @@ export class MultiChatPanel extends SidePanel { private _getChatNames: () => Promise<{ [name: string]: string }>; // Replaced command strings with callback functions: - private _openChat: (path: string) => void; + private _openChat: (name: string) => void; private _createChat: () => void; - private _closeChat: (path: string) => void; - private _moveToMain: (path: string) => void; + private _closeChat: (name: string) => void; + private _renameChatCallback: ( + oldName: string, + newName: string + ) => Promise; private _onChatsChanged?: (cb: () => void) => void; private _openChatWidget: ReactWidget; @@ -308,21 +267,16 @@ export namespace ChatPanel { export interface IOptions extends SidePanel.IOptions { rmRegistry: IRenderMimeRegistry; themeManager: IThemeManager | null; - defaultDirectory: string; chatFileExtension: string; getChatNames: () => Promise<{ [name: string]: string }>; onChatsChanged?: (cb: () => void) => void; // Callback functions instead of command strings - openChat: (path: string) => void; + openChat: (name: string) => void; createChat: () => void; - closeChat: (path: string) => void; - moveToMain: (path: string) => void; - renameChat: ( - section: ChatSection.IOptions, - path: string, - newName: string - ) => void; + closeChat: (name: string) => void; + moveToMain: (name: string) => void; + renameChat: (oldName: string, newName: string) => Promise; chatCommandRegistry?: IChatCommandRegistry; attachmentOpenerRegistry?: IAttachmentOpenerRegistry; @@ -348,19 +302,20 @@ class ChatSection extends PanelWithToolbar { this.addWidget(options.widget); this.addWidget(this._spinner); this.addClass(SECTION_CLASS); - this._defaultDirectory = options.defaultDirectory; - this._path = options.path; this._closeChat = options.closeChat; - this._renameChat = options.renameChat; this.toolbar.addClass(TOOLBAR_CLASS); - this._displayName = this._path.replace(/\.chat$/, ''); + this._displayName = options.widget.model.name ?? 'Chat'; this._updateTitle(); this._markAsRead = new ToolbarButton({ icon: readIcon, iconLabel: 'Mark chat as read', className: 'jp-mod-styled', - onClick: () => (this.model.unreadMessages = []) + onClick: () => { + if (this.model) { + this.model.unreadMessages = []; + } + } }); const renameButton = new ToolbarButton({ @@ -368,9 +323,13 @@ class ChatSection extends PanelWithToolbar { iconLabel: 'Rename chat', className: 'jp-mod-styled', onClick: async () => { - const newName = await showRenameDialog(this.title.label); - if (newName && newName.trim() && newName !== this.title.label) { - this._renameChat(this, this._path, newName.trim()); + const oldName = this.model?.name ?? 'Chat'; + const newName = await showRenameDialog(oldName); + if (this.model && newName && newName !== oldName) { + this.model.name = newName; + this._displayName = newName; + this._updateTitle(); + options.renameChat(oldName, newName); } } }); @@ -380,7 +339,9 @@ class ChatSection extends PanelWithToolbar { iconLabel: 'Move the chat to the main area', className: 'jp-mod-styled', onClick: () => { - const mainWidget = options.openChat(this._path) as Widget | undefined; + const mainWidget = options.openChat(options.widget.model.name) as + | Widget + | undefined; if (mainWidget) { mainWidget.disposed.connect(() => { @@ -395,90 +356,60 @@ class ChatSection extends PanelWithToolbar { iconLabel: 'Close the chat', className: 'jp-mod-styled', onClick: () => { - this.model.dispose(); - this._closeChat(this._path); + this.model?.dispose(); + this._closeChat(options.widget.model.name ?? ''); this.dispose(); } }); this.toolbar.addItem('markRead', this._markAsRead); - this.toolbar.addItem('rename', renameButton); this.toolbar.addItem('moveMain', moveToMain); + this.toolbar.addItem('rename', renameButton); this.toolbar.addItem('close', closeButton); this.toolbar.node.style.backgroundColor = 'js-toolbar-background'; this.toolbar.node.style.minHeight = '32px'; this.toolbar.node.style.display = 'flex'; - this.model.unreadChanged?.connect(this._unreadChanged); - this._markAsRead.enabled = this.model.unreadMessages.length > 0; + this.model?.unreadChanged?.connect(this._unreadChanged); + this._markAsRead.enabled = (this.model?.unreadMessages.length ?? 0) > 0; options.widget.node.style.height = '100%'; /** * Remove the spinner when the chat is ready. */ - this.model.ready.then(() => { + this.model?.ready.then(() => { this._spinner.dispose(); }); } - /** - * The path of the chat. - */ - get path(): string { - return this._path; - } - - /** - * The default directory of the chat. - */ - get defaultDirectory(): string { - return this._defaultDirectory; - } - - /** - * Set the default directory property. - */ - set defaultDirectory(value: string) { - this._defaultDirectory = value; - this._updateTitle(); - } - /** * The model of the widget. */ - get model(): IChatModel { - return (this.widgets[0] as ChatWidget).model; + get model(): IChatModel | null { + const first = this.widgets[0] as ChatWidget | undefined; + return first ? first.model : null; } /** * Dispose of the resources held by the widget. */ dispose(): void { - this.model.unreadChanged?.disconnect(this._unreadChanged); + const model = this.model; + if (model) { + model.unreadChanged?.disconnect(this._unreadChanged); + } super.dispose(); } /** - * Update the section's title, depending on the default directory and chat file name. - * If the chat file is in the default directory, the section's name is its relative - * path to that default directory. Otherwise, it is it absolute path. - */ + * * Update the section’s title based on the chat name. + * */ + private _updateTitle(): void { this.title.label = this._displayName; - this.title.caption = this._path; - } - - public updateDisplayName(newName: string) { - this._path = PathExt.join(this.defaultDirectory, `${newName}.chat`); - this._displayName = newName; - this._updateTitle(); - } - - public updatePath(newPath: string) { - this._path = newPath; - this._updateTitle(); + this.title.caption = this._displayName; } /** @@ -493,18 +424,11 @@ class ChatSection extends PanelWithToolbar { this._markAsRead.enabled = unread.length > 0; }; - private _defaultDirectory: string; - private _path: string; private _markAsRead: ToolbarButton; private _spinner = new Spinner(); private _displayName: string; - private _closeChat: (path: string) => void; - private _renameChat: ( - section: ChatSection, - path: string, - newName: string - ) => void; + private _closeChat: (name: string) => void; } /** @@ -516,12 +440,9 @@ export namespace ChatSection { */ export interface IOptions extends Panel.IOptions { widget: ChatWidget; - path: string; - defaultDirectory: string; - openChat: (path: string) => void; - closeChat: (path: string) => void; - moveToMain: (path: string) => void; - renameChat: (section: ChatSection, path: string, newName: string) => void; + openChat: (name: string) => void; + closeChat: (name: string) => void; + renameChat: (oldName: string, newName: string) => void; } } diff --git a/packages/jupyterlab-chat-extension/src/index.ts b/packages/jupyterlab-chat-extension/src/index.ts index 1796592b..3dba00ed 100644 --- a/packages/jupyterlab-chat-extension/src/index.ts +++ b/packages/jupyterlab-chat-extension/src/index.ts @@ -64,7 +64,7 @@ import { YChat, chatFileType } from 'jupyterlab-chat'; -import { MultiChatPanel as ChatPanel, ChatSection } from '@jupyter/chat'; +import { MultiChatPanel as ChatPanel } from '@jupyter/chat'; import { chatCommandRegistryPlugin } from './chat-commands/plugins'; import { emojiCommandsPlugin } from './chat-commands/providers/emoji'; import { mentionCommandsPlugin } from './chat-commands/providers/user-mention'; @@ -670,6 +670,87 @@ const chatCommands: JupyterFrontEndPlugin = { console.error('The command to open a chat is not initialized\n', e) ); + // Command to rename a chat + commands.addCommand(CommandIDs.renameChat, { + label: 'Rename chat', + execute: async (args: any) => { + const oldPath = args.oldPath as string; + let newPath = args.newPath as string | null; + + if (!oldPath) { + showErrorMessage('Error renaming chat', 'Missing old path'); + return; + } + + // Ask user if new name not passed in args + if (!newPath) { + const result = await InputDialog.getText({ + title: 'Rename Chat', + text: PathExt.basename(oldPath).replace(/\.chat$/, ''), // strip extension + placeholder: 'new-name' + }); + if (!result.button.accept) { + return; // user cancelled + } + newPath = result.value; + } + + if (!newPath) { + return; + } + + // Ensure `.chat` extension + if (!newPath.endsWith(chatFileType.extensions[0])) { + newPath = `${newPath}${chatFileType.extensions[0]}`; + } + + // Join with same directory + const targetDir = PathExt.dirname(oldPath); + const fullNewPath = PathExt.join(targetDir, newPath); + + try { + await app.serviceManager.contents.rename(oldPath, fullNewPath); + console.log(`Renamed chat ${oldPath} → ${fullNewPath}`); + } catch (err) { + console.error('Error renaming chat', err); + showErrorMessage('Error renaming chat', `${err}`); + } + } + }); + + // Command to close a chat + commands.addCommand(CommandIDs.closeChat, { + label: 'Close chat', + execute: async (args: any) => { + const filepath = args.filepath as string; + if (!filepath) { + console.warn('No filepath provided to closeChat'); + return; + } + + // Find widget in tracker + const widget = tracker.find(w => w.model?.name === filepath); + if (widget) { + widget.close(); // triggers dispose + console.log(`Closed chat ${filepath}`); + } else { + console.warn(`No open chat widget found for ${filepath}`); + } + } + }); + + // Optional: add to palette + if (commandPalette) { + commandPalette.addItem({ + category: 'Chat', + command: CommandIDs.renameChat + }); + commandPalette.addItem({ + category: 'Chat', + command: CommandIDs.closeChat + }); + } + // The command to focus the input of the current chat widget. commands.addCommand(CommandIDs.focusInput, { caption: 'Focus the input of the current chat widget', @@ -763,7 +844,6 @@ const chatPanel: JupyterFrontEndPlugin = { getChatNames, onChatsChanged, themeManager, - defaultDirectory, chatCommandRegistry, attachmentOpenerRegistry, inputToolbarFactory, @@ -782,30 +862,17 @@ const chatPanel: JupyterFrontEndPlugin = { moveToMain: (path: string) => { commands.execute(CommandIDs.moveToMain, { filepath: path }); }, - renameChat: ( - section: ChatSection.IOptions, - path: string, - newName: string - ) => { - if (section.widget.title.label !== newName) { - const newPath = `${defaultDirectory}/${newName}${chatFileExtension}`; - serviceManager.contents - .rename(path, newPath) - .catch(err => console.error('Rename failed:', err)); - section.widget.title.label = newName; - } + renameChat: (oldPath, newPath) => { + return commands.execute(CommandIDs.renameChat, { + oldPath, + newPath + }) as Promise; } }); chatPanel.id = 'JupyterlabChat:sidepanel'; chatPanel.title.icon = chatIcon; chatPanel.title.caption = 'Jupyter Chat'; // TODO: i18n/ - factory.widgetConfig.configChanged.connect((_, config) => { - if (config.defaultDirectory !== undefined) { - chatPanel.defaultDirectory = config.defaultDirectory; - } - }); - app.shell.add(chatPanel, 'left', { rank: 2000 }); diff --git a/packages/jupyterlab-chat/src/token.ts b/packages/jupyterlab-chat/src/token.ts index 4100014c..464a2095 100644 --- a/packages/jupyterlab-chat/src/token.ts +++ b/packages/jupyterlab-chat/src/token.ts @@ -113,7 +113,11 @@ export const CommandIDs = { /** * Move a main widget to the main area. */ - moveToMain: 'jupyterlab-chat:moveToMain' + moveToMain: 'jupyterlab-chat:moveToMain', + /** + * Rename the current chat. + */ + renameChat: 'jupyterlab-chat:renameChat' }; /** diff --git a/packages/jupyterlab-chat/src/widget.tsx b/packages/jupyterlab-chat/src/widget.tsx index 0d291374..1dbcc9d6 100644 --- a/packages/jupyterlab-chat/src/widget.tsx +++ b/packages/jupyterlab-chat/src/widget.tsx @@ -11,7 +11,7 @@ import { IMessageFooterRegistry, IInputToolbarRegistryFactory } from '@jupyter/chat'; -import { MultiChatPanel, ChatSection } from '@jupyter/chat'; +import { MultiChatPanel } from '@jupyter/chat'; import { Contents } from '@jupyterlab/services'; import { IThemeManager } from '@jupyterlab/apputils'; import { DocumentWidget } from '@jupyterlab/docregistry'; @@ -114,7 +114,6 @@ export function createMultiChatPanel(options: { return new MultiChatPanel({ rmRegistry: options.rmRegistry, themeManager: options.themeManager, - defaultDirectory: options.defaultDirectory, chatFileExtension: chatFileType.extensions[0], getChatNames, onChatsChanged, @@ -130,18 +129,11 @@ export function createMultiChatPanel(options: { moveToMain: path => { options.commands.execute(CommandIDs.moveToMain, { filepath: path }); }, - renameChat: ( - section: ChatSection.IOptions, - path: string, - newName: string - ) => { - if (section.widget.title.label !== newName) { - const newPath = `${defaultDirectory}/${newName}${chatFileExtension}`; - contentsManager - .rename(path, newPath) - .catch(err => console.error('Rename failed:', err)); - section.widget.title.label = newName; - } + renameChat: (oldPath, newPath) => { + return options.commands.execute(CommandIDs.renameChat, { + oldPath, + newPath + }) as Promise; }, chatCommandRegistry: options.chatCommandRegistry, attachmentOpenerRegistry: options.attachmentOpenerRegistry, From dff9f94431e6adb3c784c44c4c798c43e5c02a56 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Fri, 5 Sep 2025 13:43:59 +0200 Subject: [PATCH 09/15] Clean some unused code, and change section title class name to avoid duplication --- .../src/widgets/multiChatPanel.tsx | 9 +- packages/jupyterlab-chat/src/widget.tsx | 89 +------------------ 2 files changed, 3 insertions(+), 95 deletions(-) diff --git a/packages/jupyter-chat/src/widgets/multiChatPanel.tsx b/packages/jupyter-chat/src/widgets/multiChatPanel.tsx index 5cbdcdbd..8c048c6b 100644 --- a/packages/jupyter-chat/src/widgets/multiChatPanel.tsx +++ b/packages/jupyter-chat/src/widgets/multiChatPanel.tsx @@ -39,7 +39,7 @@ const SIDEPANEL_CLASS = 'jp-chat-sidepanel'; const ADD_BUTTON_CLASS = 'jp-chat-add'; const OPEN_SELECT_CLASS = 'jp-chat-open'; const SECTION_CLASS = 'jp-chat-section'; -const TOOLBAR_CLASS = 'jp-chat-toolbar'; +const TOOLBAR_CLASS = 'jp-chat-section-toolbar'; /** * Generic sidepanel widget including multiple chats and the add chat button. @@ -145,7 +145,6 @@ export class MultiChatPanel extends SidePanel { updateChatList = async (): Promise => { try { const chatNames = await this._getChatNames(); - console.log('updateChatList emits:', chatNames); this._chatNamesChanged.emit(chatNames); } catch (e) { console.error('Error getting chat files', e); @@ -367,10 +366,6 @@ class ChatSection extends PanelWithToolbar { this.toolbar.addItem('rename', renameButton); this.toolbar.addItem('close', closeButton); - this.toolbar.node.style.backgroundColor = 'js-toolbar-background'; - this.toolbar.node.style.minHeight = '32px'; - this.toolbar.node.style.display = 'flex'; - this.model?.unreadChanged?.connect(this._unreadChanged); this._markAsRead.enabled = (this.model?.unreadMessages.length ?? 0) > 0; @@ -469,10 +464,10 @@ function ChatSelect({ return ( + {Object.keys(chatNames).map(name => ( ))} - ); } diff --git a/packages/jupyterlab-chat/src/widget.tsx b/packages/jupyterlab-chat/src/widget.tsx index 1dbcc9d6..fbb1d17a 100644 --- a/packages/jupyterlab-chat/src/widget.tsx +++ b/packages/jupyterlab-chat/src/widget.tsx @@ -3,23 +3,10 @@ * Distributed under the terms of the Modified BSD License. */ -import { - ChatWidget, - IAttachmentOpenerRegistry, - IChatCommandRegistry, - IChatModel, - IMessageFooterRegistry, - IInputToolbarRegistryFactory -} from '@jupyter/chat'; -import { MultiChatPanel } from '@jupyter/chat'; -import { Contents } from '@jupyterlab/services'; -import { IThemeManager } from '@jupyterlab/apputils'; +import { ChatWidget, IChatModel } from '@jupyter/chat'; import { DocumentWidget } from '@jupyterlab/docregistry'; -import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; -import { CommandRegistry } from '@lumino/commands'; import { LabChatModel } from './model'; -import { CommandIDs, chatFileType } from './token'; const MAIN_PANEL_CLASS = 'jp-lab-chat-main-panel'; const TITLE_UNREAD_CLASS = 'jp-lab-chat-title-unread'; @@ -68,77 +55,3 @@ export class LabChatPanel extends DocumentWidget { } }; } - -export function createMultiChatPanel(options: { - commands: CommandRegistry; - contentsManager: Contents.IManager; - rmRegistry: IRenderMimeRegistry; - themeManager: IThemeManager | null; - defaultDirectory: string; - chatCommandRegistry?: IChatCommandRegistry; - attachmentOpenerRegistry?: IAttachmentOpenerRegistry; - inputToolbarFactory?: IInputToolbarRegistryFactory; - messageFooterRegistry?: IMessageFooterRegistry; - welcomeMessage?: string; -}): MultiChatPanel { - const { contentsManager, defaultDirectory } = options; - const chatFileExtension = chatFileType.extensions[0]; - - // This function replaces updateChatList's file lookup - const getChatNames = async () => { - const dirContents = await contentsManager.get(defaultDirectory); - const names: { [name: string]: string } = {}; - for (const file of dirContents.content) { - if (file.type === 'file' && file.name.endsWith(chatFileExtension)) { - const nameWithoutExt = file.name.replace(chatFileExtension, ''); - names[nameWithoutExt] = file.path; - } - } - return names; - }; - - // Hook that fires when files change - const onChatsChanged = (cb: () => void) => { - contentsManager.fileChanged.connect((_sender, change) => { - if ( - change.type === 'new' || - change.type === 'delete' || - (change.type === 'rename' && - change.oldValue?.path !== change.newValue?.path) - ) { - cb(); - } - }); - }; - - return new MultiChatPanel({ - rmRegistry: options.rmRegistry, - themeManager: options.themeManager, - chatFileExtension: chatFileType.extensions[0], - getChatNames, - onChatsChanged, - createChat: () => { - options.commands.execute(CommandIDs.createChat); - }, - openChat: path => { - options.commands.execute(CommandIDs.openChat, { filepath: path }); - }, - closeChat: path => { - options.commands.execute(CommandIDs.closeChat, { filepath: path }); - }, - moveToMain: path => { - options.commands.execute(CommandIDs.moveToMain, { filepath: path }); - }, - renameChat: (oldPath, newPath) => { - return options.commands.execute(CommandIDs.renameChat, { - oldPath, - newPath - }) as Promise; - }, - chatCommandRegistry: options.chatCommandRegistry, - attachmentOpenerRegistry: options.attachmentOpenerRegistry, - inputToolbarFactory: options.inputToolbarFactory, - messageFooterRegistry: options.messageFooterRegistry, - welcomeMessage: options.welcomeMessage - }); -} From dfef4e04e382b0185890b128a1eb62771876fc70 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Fri, 5 Sep 2025 13:47:59 +0200 Subject: [PATCH 10/15] Remove the hook to listen for chat list, in favor to updating the list from the plugin --- .../src/widgets/multiChatPanel.tsx | 10 --- .../jupyterlab-chat-extension/src/index.ts | 74 +++++++++---------- 2 files changed, 36 insertions(+), 48 deletions(-) diff --git a/packages/jupyter-chat/src/widgets/multiChatPanel.tsx b/packages/jupyter-chat/src/widgets/multiChatPanel.tsx index 8c048c6b..12de3f41 100644 --- a/packages/jupyter-chat/src/widgets/multiChatPanel.tsx +++ b/packages/jupyter-chat/src/widgets/multiChatPanel.tsx @@ -57,7 +57,6 @@ export class MultiChatPanel extends SidePanel { this._messageFooterRegistry = options.messageFooterRegistry; this._welcomeMessage = options.welcomeMessage; this._getChatNames = options.getChatNames; - this._onChatsChanged = options.onChatsChanged; // Use the passed callback functions this._openChat = options.openChat ?? (() => {}); @@ -87,12 +86,6 @@ export class MultiChatPanel extends SidePanel { const content = this.content as AccordionPanel; content.expansionToggled.connect(this._onExpansionToggled, this); - - if (this._onChatsChanged) { - this._onChatsChanged(() => { - this.updateChatList(); - }); - } } /** @@ -252,7 +245,6 @@ export class MultiChatPanel extends SidePanel { newName: string ) => Promise; - private _onChatsChanged?: (cb: () => void) => void; private _openChatWidget: ReactWidget; } @@ -266,9 +258,7 @@ export namespace ChatPanel { export interface IOptions extends SidePanel.IOptions { rmRegistry: IRenderMimeRegistry; themeManager: IThemeManager | null; - chatFileExtension: string; getChatNames: () => Promise<{ [name: string]: string }>; - onChatsChanged?: (cb: () => void) => void; // Callback functions instead of command strings openChat: (name: string) => void; diff --git a/packages/jupyterlab-chat-extension/src/index.ts b/packages/jupyterlab-chat-extension/src/index.ts index 3dba00ed..c26a9d9e 100644 --- a/packages/jupyterlab-chat-extension/src/index.ts +++ b/packages/jupyterlab-chat-extension/src/index.ts @@ -19,7 +19,8 @@ import { SelectionWatcher, chatIcon, readIcon, - IInputToolbarRegistryFactory + IInputToolbarRegistryFactory, + MultiChatPanel } from '@jupyter/chat'; import { ICollaborativeContentProvider } from '@jupyter/collaborative-drive'; import { @@ -64,7 +65,6 @@ import { YChat, chatFileType } from 'jupyterlab-chat'; -import { MultiChatPanel as ChatPanel } from '@jupyter/chat'; import { chatCommandRegistryPlugin } from './chat-commands/plugins'; import { emojiCommandsPlugin } from './chat-commands/providers/emoji'; import { mentionCommandsPlugin } from './chat-commands/providers/user-mention'; @@ -379,7 +379,7 @@ const chatCommands: JupyterFrontEndPlugin = { drive: ICollaborativeContentProvider, factory: IChatFactory, activeCellManager: IActiveCellManager | null, - chatPanel: ChatPanel | null, + chatPanel: MultiChatPanel | null, commandPalette: ICommandPalette | null, filebrowser: IDefaultFileBrowser | null, launcher: ILauncher | null, @@ -774,9 +774,9 @@ const chatCommands: JupyterFrontEndPlugin = { }; /* - * Extension providing a chat panel. + * Extension providing a multi-chat side panel. */ -const chatPanel: JupyterFrontEndPlugin = { +const chatPanel: JupyterFrontEndPlugin = { id: pluginIds.chatPanel, description: 'The chat panel widget.', autoStart: true, @@ -803,12 +803,14 @@ const chatPanel: JupyterFrontEndPlugin = { messageFooterRegistry: IMessageFooterRegistry, themeManager: IThemeManager | null, welcomeMessage: string - ): ChatPanel => { + ): MultiChatPanel => { const { commands, serviceManager } = app; const defaultDirectory = factory.widgetConfig.config.defaultDirectory || ''; + const chatFileExtension = chatFileType.extensions[0]; + // Get the chat in default directory const getChatNames = async () => { const dirContents = await serviceManager.contents.get(defaultDirectory); const names: { [name: string]: string } = {}; @@ -821,45 +823,23 @@ const chatPanel: JupyterFrontEndPlugin = { return names; }; - // Hook that fires when files change - const onChatsChanged = (cb: () => void) => { - serviceManager.contents.fileChanged.connect( - (_sender: any, change: { type: string }) => { - if ( - change.type === 'new' || - change.type === 'delete' || - change.type === 'rename' - ) { - cb(); - } - } - ); - }; - - /** - * Add Chat widget to left sidebar - */ - const chatPanel = new ChatPanel({ + const chatPanel = new MultiChatPanel({ rmRegistry, - getChatNames, - onChatsChanged, themeManager, - chatCommandRegistry, - attachmentOpenerRegistry, - inputToolbarFactory, - messageFooterRegistry, - welcomeMessage, - chatFileExtension, + getChatNames, createChat: () => { - commands.execute(CommandIDs.createChat); + commands.execute(CommandIDs.createChat, { inSidePanel: true }); }, - openChat: (path: string) => { - commands.execute(CommandIDs.openChat, { filepath: path }); + openChat: path => { + commands.execute(CommandIDs.openChat, { + filepath: path, + inSidePanel: true + }); }, - closeChat: (path: string) => { + closeChat: path => { commands.execute(CommandIDs.closeChat, { filepath: path }); }, - moveToMain: (path: string) => { + moveToMain: path => { commands.execute(CommandIDs.moveToMain, { filepath: path }); }, renameChat: (oldPath, newPath) => { @@ -867,8 +847,26 @@ const chatPanel: JupyterFrontEndPlugin = { oldPath, newPath }) as Promise; + }, + chatCommandRegistry, + attachmentOpenerRegistry, + inputToolbarFactory, + messageFooterRegistry, + welcomeMessage + }); + + // Listen for the file changes to update the chat list. + serviceManager.contents.fileChanged.connect((_sender, change) => { + if ( + change.type === 'new' || + change.type === 'delete' || + (change.type === 'rename' && + change.oldValue?.path !== change.newValue?.path) + ) { + chatPanel.updateChatList(); } }); + chatPanel.id = 'JupyterlabChat:sidepanel'; chatPanel.title.icon = chatIcon; chatPanel.title.caption = 'Jupyter Chat'; // TODO: i18n/ From 5d932c87074b64802be6a98bc2caacd70f0b5727 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Fri, 5 Sep 2025 15:18:17 +0200 Subject: [PATCH 11/15] Remove closeChat and moveToMain commands, and use openInMain callback to move the chat in the main area --- .../src/widgets/multiChatPanel.tsx | 68 ++++++++----------- .../jupyterlab-chat-extension/src/index.ts | 32 +-------- packages/jupyterlab-chat/src/token.ts | 8 --- 3 files changed, 30 insertions(+), 78 deletions(-) diff --git a/packages/jupyter-chat/src/widgets/multiChatPanel.tsx b/packages/jupyter-chat/src/widgets/multiChatPanel.tsx index 12de3f41..abac3e5c 100644 --- a/packages/jupyter-chat/src/widgets/multiChatPanel.tsx +++ b/packages/jupyter-chat/src/widgets/multiChatPanel.tsx @@ -32,7 +32,7 @@ import { } from '@jupyterlab/ui-components'; import { ISignal, Signal } from '@lumino/signaling'; import { showRenameDialog } from '../utils/renameDialog'; -import { AccordionPanel, Panel, Widget } from '@lumino/widgets'; +import { AccordionPanel, Panel } from '@lumino/widgets'; import React, { useState } from 'react'; const SIDEPANEL_CLASS = 'jp-chat-sidepanel'; @@ -59,9 +59,9 @@ export class MultiChatPanel extends SidePanel { this._getChatNames = options.getChatNames; // Use the passed callback functions - this._openChat = options.openChat ?? (() => {}); + this._openChat = options.openChat; + this._openInMain = options.openInMain; this._createChat = options.createChat ?? (() => {}); - this._closeChat = options.closeChat ?? (() => {}); this._renameChatCallback = options.renameChat ?? (() => {}); // Add chat button calls the createChat callback @@ -121,8 +121,7 @@ export class MultiChatPanel extends SidePanel { const section = new ChatSection({ widget, - openChat: this._openChat, - closeChat: this._closeChat, + openInMain: this._openInMain, renameChat: this._renameChat }); @@ -236,10 +235,9 @@ export class MultiChatPanel extends SidePanel { private _welcomeMessage?: string; private _getChatNames: () => Promise<{ [name: string]: string }>; - // Replaced command strings with callback functions: - private _openChat: (name: string) => void; private _createChat: () => void; - private _closeChat: (name: string) => void; + private _openChat: (name: string) => void; + private _openInMain?: (name: string) => void; private _renameChatCallback: ( oldName: string, newName: string @@ -263,8 +261,7 @@ export namespace ChatPanel { // Callback functions instead of command strings openChat: (name: string) => void; createChat: () => void; - closeChat: (name: string) => void; - moveToMain: (name: string) => void; + openInMain?: (name: string) => void; renameChat: (oldName: string, newName: string) => Promise; chatCommandRegistry?: IChatCommandRegistry; @@ -291,7 +288,6 @@ class ChatSection extends PanelWithToolbar { this.addWidget(options.widget); this.addWidget(this._spinner); this.addClass(SECTION_CLASS); - this._closeChat = options.closeChat; this.toolbar.addClass(TOOLBAR_CLASS); this._displayName = options.widget.model.name ?? 'Chat'; this._updateTitle(); @@ -306,6 +302,7 @@ class ChatSection extends PanelWithToolbar { } } }); + this.toolbar.addItem('markRead', this._markAsRead); const renameButton = new ToolbarButton({ iconClass: 'jp-EditIcon', @@ -322,41 +319,36 @@ class ChatSection extends PanelWithToolbar { } } }); + this.toolbar.addItem('rename', renameButton); - const moveToMain = new ToolbarButton({ - icon: launchIcon, - iconLabel: 'Move the chat to the main area', - className: 'jp-mod-styled', - onClick: () => { - const mainWidget = options.openChat(options.widget.model.name) as - | Widget - | undefined; - - if (mainWidget) { - mainWidget.disposed.connect(() => { - this.dispose(); - }); + if (options.openInMain) { + const moveToMain = new ToolbarButton({ + icon: launchIcon, + iconLabel: 'Move the chat to the main area', + className: 'jp-mod-styled', + onClick: () => { + const name = this.model.name; + this.model.dispose(); + options.openInMain?.(name); + this.dispose(); } - } - }); + }); + this.toolbar.addItem('moveMain', moveToMain); + } const closeButton = new ToolbarButton({ icon: closeIcon, iconLabel: 'Close the chat', className: 'jp-mod-styled', onClick: () => { - this.model?.dispose(); - this._closeChat(options.widget.model.name ?? ''); + this.model.dispose(); this.dispose(); } }); - this.toolbar.addItem('markRead', this._markAsRead); - this.toolbar.addItem('moveMain', moveToMain); - this.toolbar.addItem('rename', renameButton); this.toolbar.addItem('close', closeButton); - this.model?.unreadChanged?.connect(this._unreadChanged); + this.model.unreadChanged?.connect(this._unreadChanged); this._markAsRead.enabled = (this.model?.unreadMessages.length ?? 0) > 0; options.widget.node.style.height = '100%'; @@ -364,7 +356,7 @@ class ChatSection extends PanelWithToolbar { /** * Remove the spinner when the chat is ready. */ - this.model?.ready.then(() => { + this.model.ready.then(() => { this._spinner.dispose(); }); } @@ -372,9 +364,8 @@ class ChatSection extends PanelWithToolbar { /** * The model of the widget. */ - get model(): IChatModel | null { - const first = this.widgets[0] as ChatWidget | undefined; - return first ? first.model : null; + get model(): IChatModel { + return (this.widgets[0] as ChatWidget).model; } /** @@ -412,8 +403,6 @@ class ChatSection extends PanelWithToolbar { private _markAsRead: ToolbarButton; private _spinner = new Spinner(); private _displayName: string; - - private _closeChat: (name: string) => void; } /** @@ -425,8 +414,7 @@ export namespace ChatSection { */ export interface IOptions extends Panel.IOptions { widget: ChatWidget; - openChat: (name: string) => void; - closeChat: (name: string) => void; + openInMain?: (name: string) => void; renameChat: (oldName: string, newName: string) => void; } } diff --git a/packages/jupyterlab-chat-extension/src/index.ts b/packages/jupyterlab-chat-extension/src/index.ts index c26a9d9e..fd3879bb 100644 --- a/packages/jupyterlab-chat-extension/src/index.ts +++ b/packages/jupyterlab-chat-extension/src/index.ts @@ -718,37 +718,12 @@ const chatCommands: JupyterFrontEndPlugin = { } }); - // Command to close a chat - commands.addCommand(CommandIDs.closeChat, { - label: 'Close chat', - execute: async (args: any) => { - const filepath = args.filepath as string; - if (!filepath) { - console.warn('No filepath provided to closeChat'); - return; - } - - // Find widget in tracker - const widget = tracker.find(w => w.model?.name === filepath); - if (widget) { - widget.close(); // triggers dispose - console.log(`Closed chat ${filepath}`); - } else { - console.warn(`No open chat widget found for ${filepath}`); - } - } - }); - // Optional: add to palette if (commandPalette) { commandPalette.addItem({ category: 'Chat', command: CommandIDs.renameChat }); - commandPalette.addItem({ - category: 'Chat', - command: CommandIDs.closeChat - }); } // The command to focus the input of the current chat widget. @@ -836,11 +811,8 @@ const chatPanel: JupyterFrontEndPlugin = { inSidePanel: true }); }, - closeChat: path => { - commands.execute(CommandIDs.closeChat, { filepath: path }); - }, - moveToMain: path => { - commands.execute(CommandIDs.moveToMain, { filepath: path }); + openInMain: path => { + commands.execute(CommandIDs.openChat, { filepath: path }); }, renameChat: (oldPath, newPath) => { return commands.execute(CommandIDs.renameChat, { diff --git a/packages/jupyterlab-chat/src/token.ts b/packages/jupyterlab-chat/src/token.ts index 464a2095..225c8f8c 100644 --- a/packages/jupyterlab-chat/src/token.ts +++ b/packages/jupyterlab-chat/src/token.ts @@ -106,14 +106,6 @@ export const CommandIDs = { * Focus the input of the current chat. */ focusInput: 'jupyterlab-chat:focusInput', - /** - * Close the current chat. - */ - closeChat: 'jupyterlab-chat:closeChat', - /** - * Move a main widget to the main area. - */ - moveToMain: 'jupyterlab-chat:moveToMain', /** * Rename the current chat. */ From 3266f26603b18b344a50ce89f792304081ad029a Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Fri, 5 Sep 2025 17:53:22 +0200 Subject: [PATCH 12/15] Remove rename modal in favor of jupyterlab modal, make the function optional in the panel, and improve error handling --- .../jupyter-chat/src/utils/renameDialog.ts | 66 ----------------- .../src/widgets/multiChatPanel.tsx | 68 ++++++++--------- packages/jupyter-chat/style/input.css | 74 ------------------- .../jupyterlab-chat-extension/src/index.ts | 20 ++--- 4 files changed, 39 insertions(+), 189 deletions(-) delete mode 100644 packages/jupyter-chat/src/utils/renameDialog.ts diff --git a/packages/jupyter-chat/src/utils/renameDialog.ts b/packages/jupyter-chat/src/utils/renameDialog.ts deleted file mode 100644 index e30752bc..00000000 --- a/packages/jupyter-chat/src/utils/renameDialog.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright (c) Jupyter Development Team. - * Distributed under the terms of the Modified BSD License. - */ -import '../../style/input.css'; - -export async function showRenameDialog( - currentName: string -): Promise { - return new Promise(resolve => { - const modal = document.createElement('div'); - modal.className = 'rename-modal'; - - const dialog = document.createElement('div'); - dialog.className = 'rename-dialog'; - modal.appendChild(dialog); - - const title = document.createElement('h3'); - title.textContent = 'Rename Chat'; - dialog.appendChild(title); - - const input = document.createElement('input'); - input.type = 'text'; - input.value = currentName; - dialog.appendChild(input); - - const buttons = document.createElement('div'); - buttons.className = 'rename-buttons'; - - const cancelBtn = document.createElement('button'); - cancelBtn.textContent = 'Cancel'; - cancelBtn.className = 'cancel-btn'; - cancelBtn.onclick = () => { - document.body.removeChild(modal); - resolve(null); - }; - buttons.appendChild(cancelBtn); - - const okBtn = document.createElement('button'); - okBtn.textContent = 'Rename'; - okBtn.className = 'rename-ok'; - okBtn.onclick = () => { - const val = input.value.trim(); - if (val) { - document.body.removeChild(modal); - resolve(val); - } else { - input.focus(); - } - }; - buttons.appendChild(okBtn); - - dialog.appendChild(buttons); - - document.body.appendChild(modal); - input.focus(); - - input.addEventListener('keydown', e => { - if (e.key === 'Enter') { - okBtn.click(); - } else if (e.key === 'Escape') { - cancelBtn.click(); - } - }); - }); -} diff --git a/packages/jupyter-chat/src/widgets/multiChatPanel.tsx b/packages/jupyter-chat/src/widgets/multiChatPanel.tsx index abac3e5c..32091011 100644 --- a/packages/jupyter-chat/src/widgets/multiChatPanel.tsx +++ b/packages/jupyter-chat/src/widgets/multiChatPanel.tsx @@ -17,7 +17,7 @@ import { IMessageFooterRegistry, readIcon } from '../index'; -import { IThemeManager } from '@jupyterlab/apputils'; +import { InputDialog, IThemeManager } from '@jupyterlab/apputils'; import { IRenderMimeRegistry } from '@jupyterlab/rendermime'; import { addIcon, @@ -31,7 +31,6 @@ import { ToolbarButton } from '@jupyterlab/ui-components'; import { ISignal, Signal } from '@lumino/signaling'; -import { showRenameDialog } from '../utils/renameDialog'; import { AccordionPanel, Panel } from '@lumino/widgets'; import React, { useState } from 'react'; @@ -62,7 +61,7 @@ export class MultiChatPanel extends SidePanel { this._openChat = options.openChat; this._openInMain = options.openInMain; this._createChat = options.createChat ?? (() => {}); - this._renameChatCallback = options.renameChat ?? (() => {}); + this._renameChat = options.renameChat; // Add chat button calls the createChat callback const addChat = new ToolbarButton({ @@ -164,19 +163,6 @@ export class MultiChatPanel extends SidePanel { this._openChatWidget.renderPromise?.then(() => this.updateChatList()); } - /** - * Rename a chat. - */ - private _renameChat = async (oldName: string, newName: string) => { - try { - await this._renameChatCallback?.(oldName, newName); - this.updateChatList(); - console.log(`Renamed chat ${oldName} → ${newName}`); - } catch (e) { - console.error('Error renaming chat', e); - } - }; - /** * Return the index of the chat in the list (-1 if not opened). * @@ -238,10 +224,7 @@ export class MultiChatPanel extends SidePanel { private _createChat: () => void; private _openChat: (name: string) => void; private _openInMain?: (name: string) => void; - private _renameChatCallback: ( - oldName: string, - newName: string - ) => Promise; + private _renameChat?: (oldName: string, newName: string) => Promise; private _openChatWidget: ReactWidget; } @@ -262,7 +245,7 @@ export namespace ChatPanel { openChat: (name: string) => void; createChat: () => void; openInMain?: (name: string) => void; - renameChat: (oldName: string, newName: string) => Promise; + renameChat?: (oldName: string, newName: string) => Promise; chatCommandRegistry?: IChatCommandRegistry; attachmentOpenerRegistry?: IAttachmentOpenerRegistry; @@ -304,22 +287,33 @@ class ChatSection extends PanelWithToolbar { }); this.toolbar.addItem('markRead', this._markAsRead); - const renameButton = new ToolbarButton({ - iconClass: 'jp-EditIcon', - iconLabel: 'Rename chat', - className: 'jp-mod-styled', - onClick: async () => { - const oldName = this.model?.name ?? 'Chat'; - const newName = await showRenameDialog(oldName); - if (this.model && newName && newName !== oldName) { - this.model.name = newName; - this._displayName = newName; - this._updateTitle(); - options.renameChat(oldName, newName); + if (options.renameChat) { + const renameButton = new ToolbarButton({ + iconClass: 'jp-EditIcon', + iconLabel: 'Rename chat', + className: 'jp-mod-styled', + onClick: async () => { + const oldName = this.model.name ?? 'Chat'; + const result = await InputDialog.getText({ + title: 'Rename Chat', + text: this.model.name, + placeholder: 'new-name' + }); + if (!result.button.accept) { + return; // user cancelled + } + const newName = result.value; + if (this.model && newName && newName !== oldName) { + if (await options.renameChat?.(oldName, newName)) { + this.model.name = newName; + this._displayName = newName; + this._updateTitle(); + } + } } - } - }); - this.toolbar.addItem('rename', renameButton); + }); + this.toolbar.addItem('rename', renameButton); + } if (options.openInMain) { const moveToMain = new ToolbarButton({ @@ -415,7 +409,7 @@ export namespace ChatSection { export interface IOptions extends Panel.IOptions { widget: ChatWidget; openInMain?: (name: string) => void; - renameChat: (oldName: string, newName: string) => void; + renameChat?: (oldName: string, newName: string) => Promise; } } diff --git a/packages/jupyter-chat/style/input.css b/packages/jupyter-chat/style/input.css index 56abb8eb..f9d4009b 100644 --- a/packages/jupyter-chat/style/input.css +++ b/packages/jupyter-chat/style/input.css @@ -72,77 +72,3 @@ border-radius: 3px; white-space: nowrap; } - -.rename-modal { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - background-color: rgb(41 41 41 / 40%); - display: flex; - justify-content: center; - align-items: center; - z-index: 1000; -} - -.rename-dialog { - background: var(--jp-layout-color2); - padding: 1rem 1.5rem; - border-radius: 8px; - min-width: 300px; - box-shadow: 0 4px 8px rgb(24 23 23 / 26%); - color: var(--jp-ui-font-color1); - border: 1px solid var(--jp-border-color0); -} - -.rename-dialog h3 { - margin-top: 0; - color: var(--jp-ui-font-color1); -} - -.rename-dialog input[type='text'] { - width: 100%; - padding: 0.4rem 0.6rem; - margin-bottom: 1rem; - font-size: 1rem; - border: 1px solid var(--jp-border-color1); - border-radius: 3px; - background-color: var(--jp-layout-color1); - color: var(--jp-ui-font-color1); -} - -.rename-dialog input[type='text']::placeholder { - color: var(--jp-ui-font-color2); -} - -.rename-buttons { - display: flex; - justify-content: flex-end; - gap: 0.5rem; -} - -/* stylelint-disable-next-line no-descending-specificity */ -.rename-buttons button { - cursor: pointer; - padding: 0.3rem 0.7rem; - border-radius: 3px; - border: 1px solid var(--jp-border-color2); - background-color: var(--jp-layout-color1); - color: var(--jp-ui-font-color1); - font-size: 0.9rem; - transition: background-color 0.2s ease; -} - -.rename-buttons button.cancel-btn { - background-color: var(--jp-layout-color1); - border-color: var(--jp-border-color2); - color: var(--jp-ui-font-color1); -} - -.rename-buttons button.rename-ok { - font-weight: bold; - background-color: var(--jp-brand-color1); - border-color: var(--jp-brand-color1); - color: var(--jp-ui-font-color1); -} diff --git a/packages/jupyterlab-chat-extension/src/index.ts b/packages/jupyterlab-chat-extension/src/index.ts index fd3879bb..3aba117f 100644 --- a/packages/jupyterlab-chat-extension/src/index.ts +++ b/packages/jupyterlab-chat-extension/src/index.ts @@ -673,13 +673,12 @@ const chatCommands: JupyterFrontEndPlugin = { // Command to rename a chat commands.addCommand(CommandIDs.renameChat, { label: 'Rename chat', - execute: async (args: any) => { + execute: async (args: any): Promise => { const oldPath = args.oldPath as string; let newPath = args.newPath as string | null; - if (!oldPath) { showErrorMessage('Error renaming chat', 'Missing old path'); - return; + return false; } // Ask user if new name not passed in args @@ -690,13 +689,13 @@ const chatCommands: JupyterFrontEndPlugin = { placeholder: 'new-name' }); if (!result.button.accept) { - return; // user cancelled + return false; // user cancelled } newPath = result.value; } if (!newPath) { - return; + return false; } // Ensure `.chat` extension @@ -704,17 +703,14 @@ const chatCommands: JupyterFrontEndPlugin = { newPath = `${newPath}${chatFileType.extensions[0]}`; } - // Join with same directory - const targetDir = PathExt.dirname(oldPath); - const fullNewPath = PathExt.join(targetDir, newPath); - try { - await app.serviceManager.contents.rename(oldPath, fullNewPath); - console.log(`Renamed chat ${oldPath} → ${fullNewPath}`); + await app.serviceManager.contents.rename(oldPath, newPath); + return true; } catch (err) { console.error('Error renaming chat', err); showErrorMessage('Error renaming chat', `${err}`); } + return false; } }); @@ -818,7 +814,7 @@ const chatPanel: JupyterFrontEndPlugin = { return commands.execute(CommandIDs.renameChat, { oldPath, newPath - }) as Promise; + }) as Promise; }, chatCommandRegistry, attachmentOpenerRegistry, From 28b77261caf1b57ccca4f25e78b8857a38a11b0c Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Fri, 5 Sep 2025 22:31:03 +0200 Subject: [PATCH 13/15] Renaming --- .../src/components/input/toolbar-registry.tsx | 22 +++++++++++++++- packages/jupyter-chat/src/index.ts | 2 -- packages/jupyter-chat/src/token.ts | 26 ------------------- packages/jupyter-chat/src/widgets/index.ts | 2 +- ...multiChatPanel.tsx => multichat-panel.tsx} | 2 +- 5 files changed, 23 insertions(+), 31 deletions(-) delete mode 100644 packages/jupyter-chat/src/token.ts rename packages/jupyter-chat/src/widgets/{multiChatPanel.tsx => multichat-panel.tsx} (99%) diff --git a/packages/jupyter-chat/src/components/input/toolbar-registry.tsx b/packages/jupyter-chat/src/components/input/toolbar-registry.tsx index 00c7c9c9..84638abd 100644 --- a/packages/jupyter-chat/src/components/input/toolbar-registry.tsx +++ b/packages/jupyter-chat/src/components/input/toolbar-registry.tsx @@ -2,11 +2,12 @@ * Copyright (c) Jupyter Development Team. * Distributed under the terms of the Modified BSD License. */ +import { Token } from '@lumino/coreutils'; +import { ISignal, Signal } from '@lumino/signaling'; import * as React from 'react'; import { AttachButton, CancelButton, SendButton } from './buttons'; import { IInputModel } from '../../input-model'; -import { ISignal, Signal } from '@lumino/signaling'; import { IChatCommandRegistry } from '../../registers'; /** @@ -166,3 +167,22 @@ export namespace InputToolbarRegistry { return registry; } } + +/** + * A factory interface for creating a new Input Toolbar Registry + * for each Chat Panel. + */ +export interface IInputToolbarRegistryFactory { + /** + * Create a new input toolbar registry instance. + */ + create: () => IInputToolbarRegistry; +} + +/** + * The token of the factory to create an input toolbar registry. + */ +export const IInputToolbarRegistryFactory = + new Token( + '@jupyter/chat:IInputToolbarRegistryFactory' + ); diff --git a/packages/jupyter-chat/src/index.ts b/packages/jupyter-chat/src/index.ts index 61cbbfab..ad7b8f26 100644 --- a/packages/jupyter-chat/src/index.ts +++ b/packages/jupyter-chat/src/index.ts @@ -13,5 +13,3 @@ export * from './registers'; export * from './selection-watcher'; export * from './types'; export * from './widgets'; -export * from './widgets/multiChatPanel'; -export * from './token'; diff --git a/packages/jupyter-chat/src/token.ts b/packages/jupyter-chat/src/token.ts deleted file mode 100644 index abc7171d..00000000 --- a/packages/jupyter-chat/src/token.ts +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright (c) Jupyter Development Team. - * Distributed under the terms of the Modified BSD License. - */ - -import { Token } from '@lumino/coreutils'; -import { IInputToolbarRegistry } from './index'; - -/** - * A factory interface for creating a new Input Toolbar Registry - * for each Chat Panel. - */ -export interface IInputToolbarRegistryFactory { - /** - * Create a new input toolbar registry instance. - */ - create: () => IInputToolbarRegistry; -} - -/** - * The token of the factory to create an input toolbar registry. - */ -export const IInputToolbarRegistryFactory = - new Token( - '@jupyter/chat:IInputToolbarRegistryFactory' - ); diff --git a/packages/jupyter-chat/src/widgets/index.ts b/packages/jupyter-chat/src/widgets/index.ts index d196e43b..7452956e 100644 --- a/packages/jupyter-chat/src/widgets/index.ts +++ b/packages/jupyter-chat/src/widgets/index.ts @@ -6,4 +6,4 @@ export * from './chat-error'; export * from './chat-sidebar'; export * from './chat-widget'; -export * from './multiChatPanel'; +export * from './multichat-panel'; diff --git a/packages/jupyter-chat/src/widgets/multiChatPanel.tsx b/packages/jupyter-chat/src/widgets/multichat-panel.tsx similarity index 99% rename from packages/jupyter-chat/src/widgets/multiChatPanel.tsx rename to packages/jupyter-chat/src/widgets/multichat-panel.tsx index 32091011..3415e1a8 100644 --- a/packages/jupyter-chat/src/widgets/multiChatPanel.tsx +++ b/packages/jupyter-chat/src/widgets/multichat-panel.tsx @@ -194,7 +194,7 @@ export class MultiChatPanel extends SidePanel { } /** - * Triggered when a section is toogled. If the section is opened, all others + * Triggered when a section is toggled. If the section is opened, all others * sections are closed. */ private _onExpansionToggled(panel: AccordionPanel, index: number) { From 08e0a2b6db10b0b9cc89b275f2a373bbb8dec229 Mon Sep 17 00:00:00 2001 From: Nicolas Brichet Date: Sat, 6 Sep 2025 01:10:24 +0200 Subject: [PATCH 14/15] Revert wrong snapshot and test update, and fix commands test --- ui-tests/tests/commands.spec.ts | 9 +++--- .../launcher-tile-linux.png | Bin 1385 -> 1441 bytes .../menu-new-linux.png | Bin 7513 -> 7390 bytes .../tab-with-unread-linux.png | Bin 1686 -> 1164 bytes .../tab-without-unread-linux.png | Bin 1575 -> 1533 bytes ui-tests/tests/side-panel.spec.ts | 29 ++---------------- .../not-stacked-messages-linux.png | Bin 5540 -> 5117 bytes .../stacked-messages-linux.png | Bin 3709 -> 3605 bytes .../navigation-bottom-linux.png | Bin 519 -> 451 bytes .../navigation-bottom-unread-linux.png | Bin 1108 -> 956 bytes .../navigation-top-linux.png | Bin 1197 -> 1188 bytes 11 files changed, 8 insertions(+), 30 deletions(-) diff --git a/ui-tests/tests/commands.spec.ts b/ui-tests/tests/commands.spec.ts index 08c6a6e2..07913fe9 100644 --- a/ui-tests/tests/commands.spec.ts +++ b/ui-tests/tests/commands.spec.ts @@ -33,14 +33,15 @@ test.describe('#commandPalette', () => { } }); - test('should have 2 commands in palette', async ({ page }) => { + test('should have 3 commands in palette', async ({ page }) => { await expect( page.locator('#modal-command-palette li[data-command^="jupyterlab-chat"]') - ).toHaveCount(2); + ).toHaveCount(3); }); test('should create a chat with name from command palette', async ({ - page + page, + tmpPath }) => { await page .locator( @@ -49,7 +50,7 @@ test.describe('#commandPalette', () => { .click(); await fillModal(page, name); await page.waitForCondition( - async () => await page.filebrowser.contents.fileExists(`tests-commands--commandPal-bb324-h-name-from-command-palette/${FILENAME}`) + async () => await page.filebrowser.contents.fileExists(`${tmpPath}/${FILENAME}`) ); await expect(page.activity.getTabLocator(FILENAME)).toBeVisible(); }); diff --git a/ui-tests/tests/commands.spec.ts-snapshots/launcher-tile-linux.png b/ui-tests/tests/commands.spec.ts-snapshots/launcher-tile-linux.png index 8d197e0150d3141333a721edb5cd13ac876f0f12..ca959328bb3806ebbee6ed889f8296dbcdd42da7 100644 GIT binary patch literal 1441 zcmbu9`#Tc~7{`~GQ9B1uL(3`4C6`u_Ylb$P%0Z4v||W z4^A0UA(tg*HkZl$-bgmqgdx`?XQy*|&L41oc;EN?yuW?k_xpUAcsC4K8LA8b0Kix$ zw1;e7lvM;sPS%^Pe6P#Ku1F6I3Q*CdB>?~w&SB9=uju^wT%?o#g?&j<&jVy%3PGC| zb|<`Jxa6q8_<&d5?O(qnI~AU0`U_ldhK${62(_I5qRxHpGB#4;nA#RX=s8?SA?NWP zdi>FYZ+Lb&K~l;>#8wK0SIfll5nVC&*FR0L*Oiu{^X-zgyJ4RXz24rAx6*Rc&$xDs z1;UpNLLd<71iSydpP!GncUbipD%(q5Fg|{b^X~Cuqd^X5w^5u9soWw5r>~}Tw$Rwn zZfePj#Lo>hWez>P?vXmA{h*rF{1PZ%jyz3O>4C~W2O>GgR@Tut+(f$wUKzKuvBU#9L=YBw`Rm`!E;`PS~{9n991rs(|Iy)P-K#x$okI;T# z4hjLwr8hJS0~~Sg?)M7{iqp)Yu2kxW#l_25tWr^w@*I_k_NP+$;*V7>g{P^8eHYRd zSS;Iw<;f?-J7!!Nb3;_>JQauh7iia4wijLCwDj~y;?HqE=De4zq(~TyR%gO&G@VWl z2uN|L$H8M5@4{W6NELN;bqh;NYthv1bk+LW+U)g>!NI|tzM!yn9YnBKOlf0dV`Zgu zI_iuy>g4tS7!0K!M$@tTE&!$+AG9FyiYe1x>jZUY4V7gORoWgew=*X9JrcJ8| zO;36NsIW?o1*FFYF_A(!@nnwvemC_O&7D&b$gH4_I|c4=xSQXGgt?eHF8VyYJ{oSa zAE&-GylqgN+4ug7rPw}8J)?bKxp|Xj?`#O(vLo#UrnLK4`A#H`iizx_e6d(8D4$QP zx|{z}+{N&j19EP3+b0m56BBA{8+T&ShQ+!%4X^xzd;mj*@HF?ctA@21muzwA463Hi zyy>lUz1t?bF>U44XK7*Zdgc;kXnILehoPdT^hrE^FwybmZ71A7DH)lnkToo_fBn!+ zqeBox^h~lfV$+(-@;-JpuqyDlf&F@U9GbdP3^`wv`?S8ao)*h?GjslZsc8m@Dd6kD zTU!=3lUh4hHO+wYg=159M0Vjk%hsx-~De%8UiIJeN7uxd88f)Wd?;u z>2Z-}(6f)clC?wBm@QoHnZgf-P&Fo+NF2R%=t|K}p5>7kS>^(;XWY;gC`$5QIcu*h literal 1385 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 JLUC=c{{U~Qt`PtL literal 7513 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 vTsI#VJ%X??a58UlY#q@4uZ#iNxD&JTTZ5`e diff --git a/ui-tests/tests/notifications.spec.ts-snapshots/tab-with-unread-linux.png b/ui-tests/tests/notifications.spec.ts-snapshots/tab-with-unread-linux.png index 5b9a62f9d86e9cfdcdf1a183ad9dcf6246400779..9984cb5711faa8056d8bf7269a0e55abb01bbc13 100644 GIT binary patch delta 1131 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 DDBmb{ diff --git a/ui-tests/tests/notifications.spec.ts-snapshots/tab-without-unread-linux.png b/ui-tests/tests/notifications.spec.ts-snapshots/tab-without-unread-linux.png index 2ee8082c6130a37cc62926ea2e72df5027732a7a..890dbc0c5ea8d14cf9729c95346b5dcb76bf6092 100644 GIT binary patch literal 1533 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 z { async () => await page.filebrowser.contents.fileExists(FILENAME) ); - - const chatPanel = await openChat(page, FILENAME); - const button = chatPanel.getByTitle('Move the chat to the side panel'); - await button.click(); - const chatTitle = panel.locator( '.jp-SidePanel-content .jp-AccordionPanel-title' ); @@ -96,10 +91,6 @@ test.describe('#sidepanel', () => { async () => await page.filebrowser.contents.fileExists('untitled.chat') ); - const chatPanel = await openChat(page, FILENAME); - const button = chatPanel.getByTitle('Move the chat to the side panel'); - await button.click(); - const chatTitle = panel.locator( '.jp-SidePanel-content .jp-AccordionPanel-title' ); @@ -125,10 +116,6 @@ test.describe('#sidepanel', () => { async () => await page.filebrowser.contents.fileExists(FILENAME) ); - const chatPanel = await openChat(page, FILENAME); - const button = chatPanel.getByTitle('Move the chat to the side panel'); - await button.click(); - const chatTitle = panel.locator( '.jp-SidePanel-content .jp-AccordionPanel-title' ); @@ -205,10 +192,6 @@ test.describe('#sidepanel', () => { await select.selectOption(name); - const chatPanel = await openChat(page, FILENAME); - const button = chatPanel.getByTitle('Move the chat to the side panel'); - await button.click(); - const chatTitle = panel.locator( '.jp-SidePanel-content .jp-AccordionPanel-title' ); @@ -258,17 +241,11 @@ test.describe('#sidepanel', () => { await dialog.getByRole('button').getByText('Ok').click(); await expect(select.locator('option')).toHaveCount(2); - await expect(select.locator('option', { hasText: 'new-chat' })).toHaveCount(1); - - // Refresh the locator in case the old one is stale - const settings2 = await openSettings(page); - const defaultDirectory2 = settings2.locator( - 'input[label="defaultDirectory"]' - ); + await expect(select.locator('option').last()).toHaveText('new-chat'); // Changing the default directory (to root) should update the chat list. - await defaultDirectory2.clear(); - + await defaultDirectory.clear(); + // wait for the settings to be saved await expect(page.activity.getTabLocator('Settings')).toHaveAttribute( 'class', diff --git a/ui-tests/tests/ui-config.spec.ts-snapshots/not-stacked-messages-linux.png b/ui-tests/tests/ui-config.spec.ts-snapshots/not-stacked-messages-linux.png index 298fe5fa0cc1de8e0b1e1c2c8fb1d0eaeb3cb323..377dea0059a47be03c21f04fbe3f61fdebc43aaf 100644 GIT binary patch literal 5117 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 Kg1~FrKl~RleJmXS diff --git a/ui-tests/tests/ui-config.spec.ts-snapshots/stacked-messages-linux.png b/ui-tests/tests/ui-config.spec.ts-snapshots/stacked-messages-linux.png index c83feaaf7ae110fa324b7095948922cc71f10501..8f791839714c59399983ed46118d9e1068cb7f75 100644 GIT binary patch literal 3605 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 literal 3709 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 TcVGIc00000NkvXXu0mjfq0BY+ delta 1098 zcmV-Q1hxCT2h<3V8Gix*008}ONW%aC00DDSM?wIu&K&6g00a(6L_t(YiJg{TOj~6b z#-DTA9-u8P&>pBQlt}^0(7@)*NW@Vbal2Ub!c4qUNEj9}4w1Y0)QeNRcxT@N)3(Grh2!!l7Aap`C-@} z;snJ#Y0>$V+g1MV5zAimZs_b9f8m#DKNAO#ry*Tc#y1@Xt5HMiH2{K~xXcQ7=Gfu6 zjYLWUU^nGmYkxglS**(l_05F7?0c9LC6i9m`nvhdtA#mgQt_CHl=S-)b8|ez@M5lp z{QCVGm(!#G9hqf5`*{jL!^`=XPnG3S%DA241?j6l{D1j50M|dPZGOhxf<9X1KKgM& z5aqK+%GAddJ2M6Qv_|BDhJU%cmpxE3eDh&YUgE2DQiw<+{ zUK7vrqoboJU zEX%SzJw4%YxTB-PVo|8Pbo+%lLA`Y;K~m(>G=J@OyS-j-Z*Q+G%Ph;bx3@bS4#l@h zZAt(nStd<-3IG!m6@36K7E4!G7tiyNNTjW;&FOSzax%OKz^td#+T=Qr_c)xe6;PSn zR;#tMvy)+%nwpwS9svJ@lK`UY9I4XM0LJ{>K5hVZyS=`?et*xzA_u@qiRAHP4geEN z{D1x%4x;fz4nVyoNmiE|D`@TFdNS)__t)J8E)Wq)3RGq4O_C(wZaxIy+uO^6sGL_> zl}JffeM?S4QWfDnk17>{e~4U4OWB-F@Ii3L_t(YiKUi%Oj}hP$G`VdZrj_(Z7CNJ7*yU54G*K6ac*TL zVKGJ%LD7HIe~e2C8lzjXxW6=+iP_^HgkVgF7&I;tOiT?d`q(QHDG^81wt*IiK^%IXCD0ewUD>6?^}j@pQEg2!A^I?Fy60r~n|urJ?!I zt(m~xh1L73d>|Z+izKU|GPG1xuJ$={V!h%eb2foFn@wRdWd-N#!K`YdV$o)cnq+_4 z?7C``4#1-AXFJM__CGw*%>}W4E+rV@u_K}<81)3BcIWE1&}jd<6>2DWMW2|`8LM?$ z93e^4_H|QB;eWF~jynVE0FE$vK$r6o;zl;U04f_INL6J&+(O7OOGbE7? zP6a;vehk2|Cp8xujwq?*y6p+`;%C1)|8{Z!uDn@U_kX~!lSD$+*<5teq=N!>fv&)gbkwpMxcq86sodfXhvCNc%KV$O9Qiyks zW&&ut{(nz2CX(03m)D|#o|YXi&_1RT0AMO+G>Vj!NX$CnBkj&r0CmL-LHr*l2m+@{ zbO7wm5IMUX0dTUI*?*1QZg;s{`!gqsv;bz8BczYx0TiYy_Mt~dM|*mDIy*Z(9?w2l z5vu^;<9IR<<^k+;)g2B;cXzj5ucv9cqoZSKX@BWqWa9bxIeyc%&WlO8>FMdNt}cVY zV6j*%7K>7;?C9w5`~6AbSX=}kN)l;MQvd=HVK3zMdb_&1Se9*RX<-V_#-Y3KxK}GG*_?yMi#ag{3(_MjPm*X% zykuPf@IigSW|gWhPcL9;Z-~ESbw8f;;(u>0A19P#suFdYOg!6*jb)UKuucZs2ffl^ zwkt_!8}QzoU87{g=dYC|2LEd(wn_g-KimUw)Tp^|I+vzW4m56WCs*I9G?ngbC!1b} z|9tO6G$yj@)Q`t2?(iLVIMJf7G*=d8?6}Ljr2qs`>Kk7C+BTmkU=Itc zq24#;zgm+eBX?KgB+^ZFTpD(UZcGKn+=<^F9zdFk%Ft3z=V*_Yq}Ap#dv%HY3k?|M U9_#AW%>V!Z07*qoM6N<$g4aPc_y7O^ delta 1164 zcmV;71ateO39Si`F@I-CL_t(YiKUibOj}hP$G`W|-nRFaUP`&Z1_R`80nGqohH-9e zkgx|6iAaMaWR>LZfwv2L1`&1 z>mOFQ?aJ-#y*{)cf4XkW@8h|j^T|0U=lp(`kYyPF+=CadhJTwrJd*AH^U}}m+SaCG zrF?5HaAU@QXMXv?vfvL2k|;7-I$uxi&DHHMHB{y4_~G7x?ABvOST9{1e3(|()>@^u zUG1_dy1KdmK%HHCtSK8|>%hRQTRqZgo)GthL19x!VI{n@61Lixzr8WfrBcUtr4gAj zpfOgZ4Opv;>VG&##8J2L%90uDZ=;yL|bwUVpErX}YJUXK``yab)cI`FJ7jdL&2* zx#{U?v)ODg7)&OU$z;-KG(9~%KA$fk9EnN*Bv~d6S_*(aByNXXE|=MC<~Xjsy`5!Q zmSsCSIy4$hcXxLv6xz=FgCc+oEtT?evuSa8wJMj%OIK}yv|4R68a;aSD8s~ykV>Vh zseh><2%@a4Y`b*K85)`n6mbk$v0Dq^mYv@Y5d_iL*htg##zC5<>+9fiw!E;o=h`D`IaJC;@xF81kjbB=dD6bdpl)p;yQ5@>hT>J6WzQ$4H+uTm#UYqY!-()xd&+wlf%O(dSw!i!wLe<*iZxq9pf^ zy1yQrixsfPg-fO0tIL1AI$J?*t;7kWadlK4wFgF~{CA!4-yQ(z43(*)UdYkyFHNt> eXSeHO`4>IF<2O3f&QSmW002ovP6b4+LSTXvPcx Date: Sat, 6 Sep 2025 01:41:00 +0200 Subject: [PATCH 15/15] Allow section to have a name different from the chat model name --- .../src/widgets/multichat-panel.tsx | 39 ++++++++++++++----- .../jupyterlab-chat-extension/src/index.ts | 36 ++++++++++++----- packages/jupyterlab-chat/src/index.ts | 1 + packages/jupyterlab-chat/src/utils.ts | 32 +++++++++++++++ ui-tests/tests/commands.spec.ts | 3 +- ui-tests/tests/side-panel.spec.ts | 7 ++-- 6 files changed, 95 insertions(+), 23 deletions(-) create mode 100644 packages/jupyterlab-chat/src/utils.ts diff --git a/packages/jupyter-chat/src/widgets/multichat-panel.tsx b/packages/jupyter-chat/src/widgets/multichat-panel.tsx index 3415e1a8..0bd2a4fb 100644 --- a/packages/jupyter-chat/src/widgets/multichat-panel.tsx +++ b/packages/jupyter-chat/src/widgets/multichat-panel.tsx @@ -87,14 +87,21 @@ export class MultiChatPanel extends SidePanel { content.expansionToggled.connect(this._onExpansionToggled, this); } + /** + * The sections of the side panel. + */ + get sections(): ChatSection[] { + return this.widgets as ChatSection[]; + } + /** * Add a new widget to the chat panel. * * @param model - the model of the chat widget - * @param name - the name of the chat. + * @param displayName - the name of the chat. */ - addChat(model: IChatModel): ChatWidget { + addChat(model: IChatModel, displayName?: string): ChatWidget { const content = this.content as AccordionPanel; for (let i = 0; i < this.widgets.length; i++) { content.collapse(i); @@ -121,7 +128,8 @@ export class MultiChatPanel extends SidePanel { const section = new ChatSection({ widget, openInMain: this._openInMain, - renameChat: this._renameChat + renameChat: this._renameChat, + displayName }); this.addWidget(section); @@ -131,7 +139,7 @@ export class MultiChatPanel extends SidePanel { } /** - * Update the list of available chats in the default directory. + * Update the list of available chats. */ updateChatList = async (): Promise => { try { @@ -185,11 +193,11 @@ export class MultiChatPanel extends SidePanel { * Handle `change` events for the HTMLSelect component. */ private _chatSelected(event: React.ChangeEvent): void { - const path = event.target.value; - if (path === '-') { + const selection = event.target.value; + if (selection === '-') { return; } - this._openChat(path); + this._openChat(selection); event.target.selectedIndex = 0; } @@ -262,7 +270,7 @@ export namespace ChatPanel { /** * The chat section containing a chat widget. */ -class ChatSection extends PanelWithToolbar { +export class ChatSection extends PanelWithToolbar { /** * Constructor of the chat section. */ @@ -272,7 +280,8 @@ class ChatSection extends PanelWithToolbar { this.addWidget(this._spinner); this.addClass(SECTION_CLASS); this.toolbar.addClass(TOOLBAR_CLASS); - this._displayName = options.widget.model.name ?? 'Chat'; + this._displayName = + options.displayName ?? options.widget.model.name ?? 'Chat'; this._updateTitle(); this._markAsRead = new ToolbarButton({ @@ -355,6 +364,17 @@ class ChatSection extends PanelWithToolbar { }); } + /** + * The display name. + */ + get displayName(): string { + return this._displayName; + } + set displayName(value: string) { + this._displayName = value; + this._updateTitle(); + } + /** * The model of the widget. */ @@ -410,6 +430,7 @@ export namespace ChatSection { widget: ChatWidget; openInMain?: (name: string) => void; renameChat?: (oldName: string, newName: string) => Promise; + displayName?: string; } } diff --git a/packages/jupyterlab-chat-extension/src/index.ts b/packages/jupyterlab-chat-extension/src/index.ts index 3aba117f..3caba603 100644 --- a/packages/jupyterlab-chat-extension/src/index.ts +++ b/packages/jupyterlab-chat-extension/src/index.ts @@ -63,7 +63,8 @@ import { LabChatPanel, WidgetConfig, YChat, - chatFileType + chatFileType, + getDisplayName } from 'jupyterlab-chat'; import { chatCommandRegistryPlugin } from './chat-commands/plugins'; import { emojiCommandsPlugin } from './chat-commands/providers/emoji'; @@ -644,8 +645,13 @@ const chatCommands: JupyterFrontEndPlugin = { // Set the name of the model. chatModel.name = model.path; + const displayName = getDisplayName( + model.path, + widgetConfig.config.defaultDirectory + ); + // Add a chat widget to the side panel and to the tracker. - const widget = chatPanel.addChat(chatModel); + const widget = chatPanel.addChat(chatModel, displayName); factory.tracker.add(widget); } else { // The chat is opened in the main area @@ -777,13 +783,13 @@ const chatPanel: JupyterFrontEndPlugin = { ): MultiChatPanel => { const { commands, serviceManager } = app; - const defaultDirectory = factory.widgetConfig.config.defaultDirectory || ''; - const chatFileExtension = chatFileType.extensions[0]; // Get the chat in default directory const getChatNames = async () => { - const dirContents = await serviceManager.contents.get(defaultDirectory); + const dirContents = await serviceManager.contents.get( + factory.widgetConfig.config.defaultDirectory ?? '' + ); const names: { [name: string]: string } = {}; for (const file of dirContents.content) { if (file.type === 'file' && file.name.endsWith(chatFileExtension)) { @@ -822,6 +828,22 @@ const chatPanel: JupyterFrontEndPlugin = { messageFooterRegistry, welcomeMessage }); + chatPanel.id = 'JupyterlabChat:sidepanel'; + chatPanel.title.icon = chatIcon; + chatPanel.title.caption = 'Jupyter Chat'; // TODO: i18n/ + + // Update available chats and section title when default directory changed. + factory.widgetConfig.configChanged.connect((_, config) => { + if (config.defaultDirectory !== undefined) { + chatPanel.updateChatList(); + chatPanel.sections.forEach(section => { + section.displayName = getDisplayName( + section.model.name, + config.defaultDirectory + ); + }); + } + }); // Listen for the file changes to update the chat list. serviceManager.contents.fileChanged.connect((_sender, change) => { @@ -835,10 +857,6 @@ const chatPanel: JupyterFrontEndPlugin = { } }); - chatPanel.id = 'JupyterlabChat:sidepanel'; - chatPanel.title.icon = chatIcon; - chatPanel.title.caption = 'Jupyter Chat'; // TODO: i18n/ - app.shell.add(chatPanel, 'left', { rank: 2000 }); diff --git a/packages/jupyterlab-chat/src/index.ts b/packages/jupyterlab-chat/src/index.ts index bffa5bea..f3368b08 100644 --- a/packages/jupyterlab-chat/src/index.ts +++ b/packages/jupyterlab-chat/src/index.ts @@ -6,5 +6,6 @@ export * from './factory'; export * from './model'; export * from './token'; +export * from './utils'; export * from './widget'; export * from './ychat'; diff --git a/packages/jupyterlab-chat/src/utils.ts b/packages/jupyterlab-chat/src/utils.ts new file mode 100644 index 00000000..77ca95ec --- /dev/null +++ b/packages/jupyterlab-chat/src/utils.ts @@ -0,0 +1,32 @@ +/* + * Copyright (c) Jupyter Development Team. + * Distributed under the terms of the Modified BSD License. + */ + +import { PathExt } from '@jupyterlab/coreutils'; + +import { chatFileType } from './token'; + +/** + * + * @param defaultDirectory - the default directory. + * @param path - the path of the chat file. + * @returns - the display name of the chat. + */ +export function getDisplayName( + path: string, + defaultDirectory?: string +): string { + const inDefault = defaultDirectory + ? !PathExt.relative(defaultDirectory, path).startsWith('..') + : true; + + const pattern = new RegExp(`${chatFileType.extensions[0]}$`, 'g'); + return ( + inDefault + ? defaultDirectory + ? PathExt.relative(defaultDirectory, path) + : path + : '/' + path + ).replace(pattern, ''); +} diff --git a/ui-tests/tests/commands.spec.ts b/ui-tests/tests/commands.spec.ts index 07913fe9..fe2d9e83 100644 --- a/ui-tests/tests/commands.spec.ts +++ b/ui-tests/tests/commands.spec.ts @@ -50,7 +50,8 @@ test.describe('#commandPalette', () => { .click(); await fillModal(page, name); await page.waitForCondition( - async () => await page.filebrowser.contents.fileExists(`${tmpPath}/${FILENAME}`) + async () => + await page.filebrowser.contents.fileExists(`${tmpPath}/${FILENAME}`) ); await expect(page.activity.getTabLocator(FILENAME)).toBeVisible(); }); diff --git a/ui-tests/tests/side-panel.spec.ts b/ui-tests/tests/side-panel.spec.ts index cd3aed64..c7d7bfb7 100644 --- a/ui-tests/tests/side-panel.spec.ts +++ b/ui-tests/tests/side-panel.spec.ts @@ -216,7 +216,6 @@ test.describe('#sidepanel', () => { 'input[label="defaultDirectory"]' ); await defaultDirectory.pressSequentially(NEW_DIR); - // wait for the settings to be saved await expect(page.activity.getTabLocator('Settings')).toHaveAttribute( 'class', @@ -227,7 +226,7 @@ test.describe('#sidepanel', () => { /jp-mod-dirty/ ); - await expect(select.locator('option')).toHaveCount(2); + await expect(select.locator('option')).toHaveCount(1); await expect(select.locator('option').last()).toHaveText('Open a chat'); // creating a chat should populate the list. @@ -256,8 +255,8 @@ test.describe('#sidepanel', () => { /jp-mod-dirty/ ); - await expect(select.locator('option')).toHaveCount(3); - await expect(select.locator('option').nth(1)).toHaveText(name); + await expect(select.locator('option')).toHaveCount(2); + await expect(select.locator('option').last()).toHaveText(name); }); });