Skip to content

Commit

Permalink
feat(vscode): dynamically display the active selection context in cha…
Browse files Browse the repository at this point in the history
…t side panel (TabbyML#3286)

* feat(vscode):  dynamically display the active selection context in chat side panel

* update: format

* [autofix.ci] apply automated fixes

* update: error handling & update version

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
  • Loading branch information
liangfung and autofix-ci[bot] authored Oct 21, 2024
1 parent d83a33d commit 639c857
Show file tree
Hide file tree
Showing 7 changed files with 99 additions and 13 deletions.
2 changes: 1 addition & 1 deletion clients/tabby-chat-panel/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "tabby-chat-panel",
"type": "module",
"version": "0.2.0",
"version": "0.2.1",
"keywords": [],
"sideEffects": false,
"exports": {
Expand Down
2 changes: 2 additions & 0 deletions clients/tabby-chat-panel/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export interface ServerApi {
cleanError: () => void
addRelevantContext: (context: Context) => void
updateTheme: (style: string, themeClass: string) => void
updateActiveSelection: (context: Context | null) => void
}

export interface ClientApi {
Expand Down Expand Up @@ -106,6 +107,7 @@ export function createServer(api: ServerApi): ClientApi {
cleanError: api.cleanError,
addRelevantContext: api.addRelevantContext,
updateTheme: api.updateTheme,
updateActiveSelection: api.updateActiveSelection,
},
})
}
3 changes: 3 additions & 0 deletions clients/vscode/src/chat/ChatSideViewProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ export class ChatSideViewProvider implements WebviewViewProvider {
await this.webviewHelper.displayPageBasedOnServerStatus();
this.webviewHelper.addAgentEventListeners();

this.webviewHelper.syncActiveSelection(window.activeTextEditor);
this.webviewHelper.addTextEditorEventListeners();

// The event will not be triggered during the initial rendering.
webviewView.onDidChangeVisibility(() => {
if (webviewView.visible) {
Expand Down
28 changes: 28 additions & 0 deletions clients/vscode/src/chat/WebviewHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,14 @@ export class WebviewHelper {
this.client?.sendMessage(message);
}

public async syncActiveSelectionToChatPanel(context: Context | null) {
try {
await this.client?.updateActiveSelection(context);
} catch {
this.logger.warn("Active selection sync failed. Please update your Tabby server to the latest version.");
}
}

public addRelevantContext(context: Context) {
if (!this.client) {
this.pendingRelevantContexts.push(context);
Expand Down Expand Up @@ -370,6 +378,7 @@ export class WebviewHelper {

this.pendingRelevantContexts.forEach((ctx) => this.addRelevantContext(ctx));
this.pendingMessages.forEach((message) => this.sendMessageToChatPanel(message));
this.syncActiveSelection(window.activeTextEditor);

if (serverInfo.config.token) {
this.client?.cleanError();
Expand Down Expand Up @@ -412,6 +421,15 @@ export class WebviewHelper {
}
}

public syncActiveSelection(editor: TextEditor | undefined) {
if (!editor) {
return;
}

const fileContext = WebviewHelper.getFileContextFromSelection({ editor, gitProvider: this.gitProvider });
this.syncActiveSelectionToChatPanel(fileContext);
}

public addAgentEventListeners() {
this.agent.on("didChangeStatus", async (status) => {
if (status !== "disconnected") {
Expand All @@ -430,6 +448,15 @@ export class WebviewHelper {
});
}

public addTextEditorEventListeners() {
window.onDidChangeTextEditorSelection((e) => {
if (e.textEditor !== window.activeTextEditor) {
return;
}
this.syncActiveSelection(e.textEditor);
});
}

public async displayPageBasedOnServerStatus() {
// At this point, if the server instance is not set up, agent.status is 'notInitialized'.
// We check for the presence of the server instance by verifying serverInfo.health["webserver"].
Expand Down Expand Up @@ -499,6 +526,7 @@ export class WebviewHelper {
message: msg,
relevantContext: [],
};
// FIXME: after synchronizing the activeSelection, perhaps there's no need to include activeSelection in the message.
if (editor) {
const fileContext = WebviewHelper.getFileContextFromSelection({ editor, gitProvider: this.gitProvider });
if (fileContext)
Expand Down
24 changes: 22 additions & 2 deletions ee/tabby-ui/app/chat/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ export default function ChatPage() {
const [pendingRelevantContexts, setPendingRelevantContexts] = useState<
Context[]
>([])
const [pendingActiveSelection, setPendingActiveSelection] =
useState<Context | null>(null)
const [errorMessage, setErrorMessage] = useState<ErrorMessage | null>(null)
const [isRefreshLoading, setIsRefreshLoading] = useState(false)

Expand Down Expand Up @@ -89,6 +91,14 @@ export default function ChatPage() {
}
}

const updateActiveSelection = (ctx: Context | null) => {
if (chatRef.current) {
chatRef.current.updateActiveSelection(ctx)
} else {
setPendingActiveSelection(ctx)
}
}

const server = useServer({
init: (request: InitRequest) => {
if (chatRef.current) return
Expand Down Expand Up @@ -135,6 +145,9 @@ export default function ChatPage() {
// Sync with edit theme
document.documentElement.className =
themeClass + ` client client-${client}`
},
updateActiveSelection: context => {
return updateActiveSelection(context)
}
})

Expand Down Expand Up @@ -233,11 +246,18 @@ export default function ChatPage() {
prevWidthRef.current = width
}, [width, chatLoaded])

const clearPendingState = () => {
setPendingRelevantContexts([])
setPendingMessages([])
setPendingActiveSelection(null)
}

const onChatLoaded = () => {
pendingRelevantContexts.forEach(addRelevantContext)
pendingMessages.forEach(sendMessage)
setPendingRelevantContexts([])
setPendingMessages([])
updateActiveSelection(pendingActiveSelection)

clearPendingState()
setChatLoaded(true)
}

Expand Down
36 changes: 28 additions & 8 deletions ee/tabby-ui/components/chat/chat-panel.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { RefObject } from 'react'
import type { UseChatHelpers } from 'ai/react'
import type { Context } from 'tabby-chat-panel'

import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
Expand Down Expand Up @@ -49,7 +50,8 @@ function ChatPanelRenderer(
qaPairs,
isLoading,
relevantContext,
removeRelevantContext
removeRelevantContext,
activeSelection
} = React.useContext(ChatContext)

React.useImperativeHandle(
Expand Down Expand Up @@ -102,21 +104,29 @@ function ChatPanelRenderer(
)}
</div>
<div className="border-t bg-background px-4 py-2 shadow-lg sm:space-y-4 sm:rounded-t-xl sm:border md:py-4">
{relevantContext.length > 0 && (
{(!!activeSelection || relevantContext.length > 0) && (
<div className="flex flex-wrap gap-2">
{activeSelection ? (
<Badge
variant="outline"
key={`${activeSelection.filepath}_active_selection`}
className="inline-flex items-center gap-0.5 rounded text-sm font-semibold"
>
<span className="text-foreground">
{getContextLabel(activeSelection)}
</span>
</Badge>
) : null}
{relevantContext.map((item, idx) => {
const [fileName] = item.filepath.split('/').slice(-1)
const line =
item.range.start === item.range.end
? `${item.range.start}`
: `${item.range.start}-${item.range.end}`
return (
<Badge
variant="outline"
key={item.filepath + idx}
className="inline-flex items-center gap-0.5 rounded text-sm font-semibold"
>
<span className="text-foreground">{`${fileName}: ${line}`}</span>
<span className="text-foreground">
{getContextLabel(item)}
</span>
<IconRemove
className="cursor-pointer text-muted-foreground transition-all hover:text-red-300"
onClick={removeRelevantContext.bind(null, idx)}
Expand Down Expand Up @@ -144,3 +154,13 @@ function ChatPanelRenderer(
export const ChatPanel = React.forwardRef<ChatPanelRef, ChatPanelProps>(
ChatPanelRenderer
)

function getContextLabel(context: Context) {
const [fileName] = context.filepath.split('/').slice(-1)
const line =
context.range.start === context.range.end
? `${context.range.start}`
: `${context.range.start}-${context.range.end}`

return `${fileName}: ${line}`
}
17 changes: 15 additions & 2 deletions ee/tabby-ui/components/chat/chat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ type ChatContextValue = {
onCopyContent?: (value: string) => void
onApplyInEditor?: (value: string) => void
relevantContext: Context[]
activeSelection: Context | null
removeRelevantContext: (index: number) => void
chatInputRef: RefObject<HTMLTextAreaElement>
}
Expand All @@ -56,6 +57,7 @@ export interface ChatRef {
isLoading: boolean
addRelevantContext: (context: Context) => void
focus: () => void
updateActiveSelection: (context: Context | null) => void
}

interface ChatProps extends React.ComponentProps<'div'> {
Expand Down Expand Up @@ -104,6 +106,9 @@ function ChatRenderer(
const [qaPairs, setQaPairs] = React.useState(initialMessages ?? [])
const [input, setInput] = React.useState<string>('')
const [relevantContext, setRelevantContext] = React.useState<Context[]>([])
const [activeSelection, setActiveSelection] = React.useState<Context | null>(
null
)
const chatPanelRef = React.useRef<ChatPanelRef>(null)

const {
Expand Down Expand Up @@ -348,6 +353,8 @@ function ChatRenderer(
const newUserMessage = {
...userMessage,
message: userMessage.message + selectCodeSnippet,
// For forward compatibility
activeSelection: activeSelection || userMessage.activeContext,
// If no id is provided, set a fallback id.
id: userMessage.id ?? nanoid()
}
Expand Down Expand Up @@ -406,6 +413,10 @@ function ChatRenderer(
onThreadUpdates?.(qaPairs)
}, [qaPairs])

const updateActiveSelection = (ctx: Context | null) => {
setActiveSelection(ctx)
}

React.useImperativeHandle(
ref,
() => {
Expand All @@ -414,7 +425,8 @@ function ChatRenderer(
stop,
isLoading,
addRelevantContext,
focus: () => chatPanelRef.current?.focus()
focus: () => chatPanelRef.current?.focus(),
updateActiveSelection
}
},
[]
Expand Down Expand Up @@ -448,7 +460,8 @@ function ChatRenderer(
onApplyInEditor,
relevantContext,
removeRelevantContext,
chatInputRef
chatInputRef,
activeSelection
}}
>
<div className="flex justify-center overflow-x-hidden">
Expand Down

0 comments on commit 639c857

Please sign in to comment.