Skip to content

Commit

Permalink
feat(tabby-chat-panel): adding capability check in tabby-ui and addin…
Browse files Browse the repository at this point in the history
…g applyInEditorV2 (#3432)

* feat: add hasCapability method to ClientApi interface

* feat: adding custom createThread implementation

* feat: add createThreadInsideIframe module and update dependencies

* [autofix.ci] apply automated fixes

* feat: add CHECK_CAPABILITY method to createThread function

* feat: update server capabilities in ChatPage component

* [autofix.ci] apply automated fixes

* feat: add ChatContext to AssistantMessageSection component

* feat(tabby-thread): adding tabby threads package

* feat(tabby-threads): add customized version of @quilted/threads for Tabby project, also added readme

* feat: add CHECK_CAPABILITY method to target.ts

* feat: update dependencies for tabby-chat-panel and vscode clients

* chore: delete custom thread

* chore: remove custom thread

* feat: adding apply in editor v2

* feat: implement applyEditorV2 on vscode

* feat(web-ui): adding capability check, also auto apply applyEditor v1 and v2

* chore: adding a new interface to encapsulate client apis

* chore: Update createClient function parameter name in index.ts and react.ts

- In index.ts, update the second parameter of the createClient function from `api: ClientApi` to `api: ClientApiMethods`.
- In react.ts, update the second parameter of the useClient function from `api: ClientApi` to `api: ClientApiMethods`.

* [autofix.ci] apply automated fixes

* chore: update tabby-chat-panel to version 0.3.0

* feat: Add clientApiKeys array to export available client API methods

The `clientApiKeys` array is added to export the available client API methods. This array includes keys for methods such as `navigate`, `refresh`, `onSubmitMessage`, `onApplyInEditor`, `onApplyInEditorV2`, `onLoaded`, `onCopy`, and `onKeyboardEvent`. This change enhances the functionality of the `ClientApi` interface.

* chore: Update createClient and useClient function parameter names

Update the second parameter of the createClient function in index.ts and the useClient function in react.ts from `api: ClientApi` to `api: ClientApiMethods`. This change improves the clarity and consistency of the function parameter names.

* chore: Update createClient and useClient function parameter names

* chore: Update createClient and useClient function parameter names

Update the second parameter of the createClient function in index.ts and the useClient function in react.ts from `api: ClientApi` to `api: ClientApiMethods`. This change improves the clarity and consistency of the function parameter names.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
  • Loading branch information
Sma1lboy and autofix-ci[bot] authored Nov 30, 2024
1 parent 6236c30 commit 5c15a7c
Show file tree
Hide file tree
Showing 11 changed files with 141 additions and 56 deletions.
20 changes: 19 additions & 1 deletion clients/tabby-chat-panel/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,14 @@ export interface ClientApiMethods {

onSubmitMessage: (msg: string, relevantContext?: Context[]) => Promise<void>

onApplyInEditor: (content: string, opts?: { languageId: string, smart: boolean }) => void
// apply content into active editor, version 1, not support smart apply
onApplyInEditor: (content: string) => void

// version 2, support smart apply and normal apply
onApplyInEditorV2?: (
content: string,
opts?: { languageId: string, smart: boolean }
) => void

// On current page is loaded.
onLoaded: (params?: OnLoadedParams | undefined) => void
Expand All @@ -78,6 +85,17 @@ export interface ClientApi extends ClientApiMethods {
hasCapability: (method: keyof ClientApiMethods) => Promise<boolean>
}

export const clientApiKeys: (keyof ClientApiMethods)[] = [
'navigate',
'refresh',
'onSubmitMessage',
'onApplyInEditor',
'onApplyInEditorV2',
'onLoaded',
'onCopy',
'onKeyboardEvent',
]

export interface ChatMessage {
message: string

Expand Down
73 changes: 41 additions & 32 deletions clients/vscode/src/chat/WebviewHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,6 +394,36 @@ export class WebviewHelper {
}

public createChatClient(webview: Webview) {
const getIndentInfo = (document: TextDocument, selection: Selection) => {
// Determine the indentation for the content
// The calculation is based solely on the indentation of the first line
const lineText = document.lineAt(selection.start.line).text;
const match = lineText.match(/^(\s*)/);
const indent = match ? match[0] : "";

// Determine the indentation for the content's first line
// Note:
// If using spaces, selection.start.character = 1 means 1 space
// If using tabs, selection.start.character = 1 means 1 tab
const indentUnit = indent[0];
const indentAmountForTheFirstLine = Math.max(indent.length - selection.start.character, 0);
const indentForTheFirstLine = indentUnit?.repeat(indentAmountForTheFirstLine) || "";

return { indent, indentForTheFirstLine };
};

const applyInEditor = (editor: TextEditor, content: string) => {
const document = editor.document;
const selection = editor.selection;
const { indent, indentForTheFirstLine } = getIndentInfo(document, selection);
// Indent the content
const indentedContent = indentForTheFirstLine + content.replaceAll("\n", "\n" + indent);
// Apply into the editor
editor.edit((editBuilder) => {
editBuilder.replace(selection, indentedContent);
});
};

return createClient(webview, {
navigate: async (context: Context, opts?: NavigateOpts) => {
if (opts?.openInEditor) {
Expand Down Expand Up @@ -444,41 +474,20 @@ export class WebviewHelper {
// FIXME: maybe deduplicate on chatMessage.relevantContext
this.sendMessage(chatMessage);
},
onApplyInEditor: async (content: string, opts?: { languageId: string; smart: boolean }) => {
const getIndentInfo = (document: TextDocument, selection: Selection) => {
// Determine the indentation for the content
// The calculation is based solely on the indentation of the first line
const lineText = document.lineAt(selection.start.line).text;
const match = lineText.match(/^(\s*)/);
const indent = match ? match[0] : "";

// Determine the indentation for the content's first line
// Note:
// If using spaces, selection.start.character = 1 means 1 space
// If using tabs, selection.start.character = 1 means 1 tab
const indentUnit = indent[0];
const indentAmountForTheFirstLine = Math.max(indent.length - selection.start.character, 0);
const indentForTheFirstLine = indentUnit?.repeat(indentAmountForTheFirstLine) || "";

return { indent, indentForTheFirstLine };
};

const applyInEditor = (editor: TextEditor) => {
const document = editor.document;
const selection = editor.selection;
const { indent, indentForTheFirstLine } = getIndentInfo(document, selection);
// Indent the content
const indentedContent = indentForTheFirstLine + content.replaceAll("\n", "\n" + indent);
// Apply into the editor
editor.edit((editBuilder) => {
editBuilder.replace(selection, indentedContent);
});
};
onApplyInEditor: async (content: string) => {
const editor = window.activeTextEditor;
if (!editor) {
window.showErrorMessage("No active editor found.");
return;
}
applyInEditor(editor, content);
},
onApplyInEditorV2: async (content: string, opts?: { languageId: string; smart: boolean }) => {
const smartApplyInEditor = async (editor: TextEditor, opts: { languageId: string; smart: boolean }) => {
if (editor.document.languageId !== opts.languageId) {
this.logger.debug("Editor's languageId:", editor.document.languageId, "opts.languageId:", opts.languageId);
window.showInformationMessage("The active editor is not in the correct language. Did normal apply.");
applyInEditor(editor);
applyInEditor(editor, content);
return;
}

Expand Down Expand Up @@ -524,7 +533,7 @@ export class WebviewHelper {
return;
}
if (!opts || !opts.smart) {
applyInEditor(editor);
applyInEditor(editor, content);
} else {
smartApplyInEditor(editor, opts);
}
Expand Down
3 changes: 2 additions & 1 deletion clients/vscode/src/chat/chatPanel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ export function createClient(webview: Webview, api: ClientApiMethods): ServerApi
refresh: api.refresh,
onSubmitMessage: api.onSubmitMessage,
onApplyInEditor: api.onApplyInEditor,
onCopy: api.onCopy,
onApplyInEditorV2: api.onApplyInEditorV2,
onLoaded: api.onLoaded,
onCopy: api.onCopy,
onKeyboardEvent: api.onKeyboardEvent,
},
});
Expand Down
20 changes: 19 additions & 1 deletion ee/tabby-ui/app/chat/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,10 @@ export default function ChatPage() {
const isInEditor = !!client || undefined
const useMacOSKeyboardEventHandler = useRef<boolean>()

// server feature support check
const [supportsOnApplyInEditorV2, setSupportsOnApplyInEditorV2] =
useState(false)

const sendMessage = (message: ChatMessage) => {
if (chatRef.current) {
chatRef.current.sendUserChat(message)
Expand Down Expand Up @@ -227,6 +231,14 @@ export default function ChatPage() {
server?.onLoaded({
apiVersion: TABBY_CHAT_PANEL_API_VERSION
})

const checkCapabilities = async () => {
server
?.hasCapability('onApplyInEditorV2')
.then(setSupportsOnApplyInEditorV2)
}

checkCapabilities()
}
}, [server])

Expand Down Expand Up @@ -369,7 +381,13 @@ export default function ChatPage() {
maxWidth={client === 'vscode' ? '5xl' : undefined}
onCopyContent={isInEditor && server?.onCopy}
onSubmitMessage={isInEditor && server?.onSubmitMessage}
onApplyInEditor={isInEditor && server?.onApplyInEditor}
onApplyInEditor={
isInEditor &&
(supportsOnApplyInEditorV2
? server?.onApplyInEditorV2
: server?.onApplyInEditor)
}
supportsOnApplyInEditorV2={supportsOnApplyInEditorV2}
/>
</ErrorBoundary>
)
Expand Down
2 changes: 1 addition & 1 deletion ee/tabby-ui/app/files/components/chat-side-bar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export const ChatSideBar: React.FC<ChatSideBarProps> = ({
})
},
async onSubmitMessage(_msg, _relevantContext) {},
onApplyInEditor(_content, _args) {},
onApplyInEditor(_content) {},
onLoaded() {},
onCopy(_content) {},
onKeyboardEvent() {}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ import {
TooltipContent,
TooltipTrigger
} from '@/components/ui/tooltip'
import { ChatContext } from '@/components/chat/chat'
import { CodeReferences } from '@/components/chat/code-references'
import { CopyButton } from '@/components/copy-button'
import {
Expand Down Expand Up @@ -94,6 +95,8 @@ export function AssistantMessageSection({
onUpdateMessage
} = useContext(SearchContext)

const { supportsOnApplyInEditorV2 } = useContext(ChatContext)

const [isEditing, setIsEditing] = useState(false)
const [showMoreSource, setShowMoreSource] = useState(false)
const [relevantCodeHighlightIndex, setRelevantCodeHighlightIndex] = useState<
Expand Down Expand Up @@ -328,6 +331,7 @@ export function AssistantMessageSection({
contextInfo={contextInfo}
fetchingContextInfo={fetchingContextInfo}
canWrapLongLines={!isLoading}
supportsOnApplyInEditorV2={supportsOnApplyInEditorV2}
/>
{/* if isEditing, do not display error message block */}
{message.error && <ErrorMessageBlock error={message.error} />}
Expand Down
4 changes: 3 additions & 1 deletion ee/tabby-ui/app/search/components/user-message-section.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { HTMLAttributes, useContext } from 'react'

import { cn } from '@/lib/utils'
import { ChatContext } from '@/components/chat/chat'
import { MessageMarkdown } from '@/components/message-markdown'

import { ConversationMessage, SearchContext } from './search'
Expand All @@ -15,12 +16,13 @@ export function UserMessageSection({
...props
}: QuestionBlockProps) {
const { contextInfo, fetchingContextInfo } = useContext(SearchContext)

const { supportsOnApplyInEditorV2 } = useContext(ChatContext)
return (
<div className={cn('font-semibold', className)} {...props}>
<MessageMarkdown
message={message.content}
contextInfo={contextInfo}
supportsOnApplyInEditorV2={supportsOnApplyInEditorV2}
fetchingContextInfo={fetchingContextInfo}
className="text-xl prose-p:mb-2 prose-p:mt-0"
headline
Expand Down
22 changes: 12 additions & 10 deletions ee/tabby-ui/components/chat/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,14 @@ type ChatContextValue = {
onClearMessages: () => void
container?: HTMLDivElement
onCopyContent?: (value: string) => void
onApplyInEditor?: (
content: string,
opts?: { languageId: string; smart: boolean }
) => void
onApplyInEditor?:
| ((content: string) => void)
| ((content: string, opts?: { languageId: string; smart: boolean }) => void)
relevantContext: Context[]
activeSelection: Context | null
removeRelevantContext: (index: number) => void
chatInputRef: RefObject<HTMLTextAreaElement>
supportsOnApplyInEditorV2: boolean
}

export const ChatContext = React.createContext<ChatContextValue>(
Expand Down Expand Up @@ -80,11 +80,11 @@ interface ChatProps extends React.ComponentProps<'div'> {
promptFormClassname?: string
onCopyContent?: (value: string) => void
onSubmitMessage?: (msg: string, relevantContext?: Context[]) => Promise<void>
onApplyInEditor?: (
content: string,
opts?: { languageId: string; smart: boolean }
) => void
onApplyInEditor?:
| ((content: string) => void)
| ((content: string, opts?: { languageId: string; smart: boolean }) => void)
chatInputRef: RefObject<HTMLTextAreaElement>
supportsOnApplyInEditorV2: boolean
}

function ChatRenderer(
Expand All @@ -104,7 +104,8 @@ function ChatRenderer(
onCopyContent,
onSubmitMessage,
onApplyInEditor,
chatInputRef
chatInputRef,
supportsOnApplyInEditorV2
}: ChatProps,
ref: React.ForwardedRef<ChatRef>
) {
Expand Down Expand Up @@ -531,7 +532,8 @@ function ChatRenderer(
relevantContext,
removeRelevantContext,
chatInputRef,
activeSelection
activeSelection,
supportsOnApplyInEditorV2
}}
>
<div className="flex justify-center overflow-x-hidden">
Expand Down
18 changes: 14 additions & 4 deletions ee/tabby-ui/components/chat/question-answer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,8 @@ function UserMessageCard(props: { message: UserMessage }) {
const { message } = props
const [{ data }] = useMe()
const selectContext = message.selectContext
const { onNavigateToContext } = React.useContext(ChatContext)
const { onNavigateToContext, supportsOnApplyInEditorV2 } =
React.useContext(ChatContext)
const selectCodeSnippet = React.useMemo(() => {
if (!selectContext?.content) return ''
const language = selectContext?.filepath
Expand Down Expand Up @@ -162,7 +163,11 @@ function UserMessageCard(props: { message: UserMessage }) {

<div className="group relative flex w-full justify-between gap-x-2">
<div className="flex-1 space-y-2 overflow-hidden px-1 md:ml-4">
<MessageMarkdown message={message.message} canWrapLongLines />
<MessageMarkdown
message={message.message}
canWrapLongLines
supportsOnApplyInEditorV2={supportsOnApplyInEditorV2}
/>
<div className="hidden md:block">
<UserMessageCardActions {...props} />
</div>
Expand Down Expand Up @@ -252,8 +257,12 @@ function AssistantMessageCard(props: AssistantMessageCardProps) {
enableRegenerating,
...rest
} = props
const { onNavigateToContext, onApplyInEditor, onCopyContent } =
React.useContext(ChatContext)
const {
onNavigateToContext,
onApplyInEditor,
onCopyContent,
supportsOnApplyInEditorV2
} = React.useContext(ChatContext)
const [relevantCodeHighlightIndex, setRelevantCodeHighlightIndex] =
React.useState<number | undefined>(undefined)
const serverCode: Array<Context> = React.useMemo(() => {
Expand Down Expand Up @@ -389,6 +398,7 @@ function AssistantMessageCard(props: AssistantMessageCardProps) {
onCodeCitationMouseEnter={onCodeCitationMouseEnter}
onCodeCitationMouseLeave={onCodeCitationMouseLeave}
canWrapLongLines={!isLoading}
supportsOnApplyInEditorV2={supportsOnApplyInEditorV2}
/>
{!!message.error && <ErrorMessageBlock error={message.error} />}
</>
Expand Down
Loading

0 comments on commit 5c15a7c

Please sign in to comment.