From 386c0b6799a88e564ef01bcd81baf97bdb0d3d79 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 6 Feb 2025 09:18:25 -0800 Subject: [PATCH 01/10] chore: Remove hard-coded 'deep-cody' and replace with constant (#6958) This bugged me while reading and to warm up to the codebase, I decided to replace the hardcoded strings with the reference. Can't do it in `lib/shared`, but in `vscode`. ## Test plan - Run Cody and see that agentic chat works. --- lib/shared/src/models/client.ts | 4 +++- .../src/chat/agentic/CodyToolProvider.test.ts | 5 +++-- vscode/src/chat/agentic/DeepCody.ts | 3 ++- vscode/src/chat/agentic/ToolboxManager.ts | 3 ++- vscode/src/chat/chat-view/ChatController.ts | 16 ++++++++++---- .../src/chat/chat-view/handlers/registry.ts | 4 ++-- vscode/src/chat/chat-view/prompt.ts | 4 ++-- vscode/webviews/chat/Transcript.tsx | 13 +++++++++--- .../chat/cells/contextCell/ContextCell.tsx | 7 +++++-- .../assistant/AssistantMessageCell.tsx | 5 +++-- vscode/webviews/components/ChatModelIcon.tsx | 3 ++- .../ModelSelectField.story.tsx | 3 ++- .../modelSelectField/ModelSelectField.tsx | 21 ++++++++++++------- 13 files changed, 61 insertions(+), 30 deletions(-) diff --git a/lib/shared/src/models/client.ts b/lib/shared/src/models/client.ts index 94b48a15385d..2b47ba6859dc 100644 --- a/lib/shared/src/models/client.ts +++ b/lib/shared/src/models/client.ts @@ -16,12 +16,14 @@ export function getExperimentalClientModelByFeatureFlag(flag: FeatureFlag): Serv } } +export const DeepCodyAgentID = 'deep-cody' + function getDeepCodyServerModel(): ServerModel { return { // This modelRef does not exist in the backend and is used to identify the model in the client. modelRef: 'sourcegraph::2023-06-01::deep-cody', displayName: 'Agentic chat', - modelName: 'deep-cody', + modelName: DeepCodyAgentID, capabilities: ['chat'], category: 'accuracy', status: 'experimental' as ModelTag.Experimental, diff --git a/vscode/src/chat/agentic/CodyToolProvider.test.ts b/vscode/src/chat/agentic/CodyToolProvider.test.ts index bba749b2cf84..a52e289e2190 100644 --- a/vscode/src/chat/agentic/CodyToolProvider.test.ts +++ b/vscode/src/chat/agentic/CodyToolProvider.test.ts @@ -1,4 +1,5 @@ import { type ContextItem, ContextItemSource, ps } from '@sourcegraph/cody-shared' +import { DeepCodyAgentID } from '@sourcegraph/cody-shared/src/models/client' import { Observable } from 'observable-fns' import { beforeEach, describe, expect, it, vi } from 'vitest' import { URI } from 'vscode-uri' @@ -91,7 +92,7 @@ describe('CodyToolProvider', () => { it('should not include CLI tool if shell is disabled', () => { vi.spyOn(toolboxManager, 'getSettings').mockReturnValue({ - agent: { name: 'deep-cody' }, + agent: { name: DeepCodyAgentID }, shell: { enabled: false }, }) const tools = CodyToolProvider.getTools() @@ -100,7 +101,7 @@ describe('CodyToolProvider', () => { it('should include CLI tool if shell is enabled', () => { vi.spyOn(toolboxManager, 'getSettings').mockReturnValue({ - agent: { name: 'deep-cody' }, + agent: { name: DeepCodyAgentID }, shell: { enabled: true }, }) const tools = CodyToolProvider.getTools() diff --git a/vscode/src/chat/agentic/DeepCody.ts b/vscode/src/chat/agentic/DeepCody.ts index c01a0c497c31..f1d4986b151c 100644 --- a/vscode/src/chat/agentic/DeepCody.ts +++ b/vscode/src/chat/agentic/DeepCody.ts @@ -18,6 +18,7 @@ import { telemetryRecorder, wrapInActiveSpan, } from '@sourcegraph/cody-shared' +import { DeepCodyAgentID } from '@sourcegraph/cody-shared/src/models/client' import { getContextFromRelativePath } from '../../commands/context/file-path' import { forkSignal } from '../../completions/utils' import { getCategorizedMentions, isUserAddedItem } from '../../prompt-builder/utils' @@ -164,7 +165,7 @@ export class DeepCodyAgent { requestID, model: DeepCodyAgent.model, traceId: span.spanContext().traceId, - chatAgent: 'deep-cody', + chatAgent: DeepCodyAgentID, }, metadata: { loop: this.stats.loop, // Number of loops run. diff --git a/vscode/src/chat/agentic/ToolboxManager.ts b/vscode/src/chat/agentic/ToolboxManager.ts index 86076ec3b94f..9d89f19862f1 100644 --- a/vscode/src/chat/agentic/ToolboxManager.ts +++ b/vscode/src/chat/agentic/ToolboxManager.ts @@ -11,6 +11,7 @@ import { startWith, userProductSubscription, } from '@sourcegraph/cody-shared' +import { DeepCodyAgentID } from '@sourcegraph/cody-shared/src/models/client' import { type Observable, Subject, map } from 'observable-fns' import { env } from 'vscode' import { DeepCodyAgent } from './DeepCody' @@ -51,7 +52,7 @@ class ToolboxManager { const shellError = this.getFeatureError('shell') // TODO: Remove hard-coded agent once we have a proper agentic chat selection UI return { - agent: { name: this.isRateLimited ? undefined : 'deep-cody' }, + agent: { name: this.isRateLimited ? undefined : DeepCodyAgentID }, shell: { enabled: shellError === undefined, error: shellError, diff --git a/vscode/src/chat/chat-view/ChatController.ts b/vscode/src/chat/chat-view/ChatController.ts index 9e2ce9c6e8a9..10298dcae8fb 100644 --- a/vscode/src/chat/chat-view/ChatController.ts +++ b/vscode/src/chat/chat-view/ChatController.ts @@ -73,6 +73,7 @@ import { type Span, context } from '@opentelemetry/api' import { captureException } from '@sentry/core' import type { SubMessage } from '@sourcegraph/cody-shared/src/chat/transcript/messages' import { resolveAuth } from '@sourcegraph/cody-shared/src/configuration/auth-resolver' +import { DeepCodyAgentID } from '@sourcegraph/cody-shared/src/models/client' import type { TelemetryEventParameters } from '@sourcegraph/telemetry' import { Subject, map } from 'observable-fns' import type { URI } from 'vscode-uri' @@ -458,7 +459,10 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv let auth: AuthCredentials | undefined = undefined if (token) { - auth = { credentials: { token, source: 'paste' }, serverEndpoint: endpoint } + auth = { + credentials: { token, source: 'paste' }, + serverEndpoint: endpoint, + } } else { const { configuration } = await currentResolvedConfig() auth = await resolveAuth(endpoint, configuration, secretStorage) @@ -667,7 +671,7 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv firstResultFromOperation(ChatBuilder.resolvedModelForChat(this.chatBuilder)) ) this.chatBuilder.setSelectedModel(model) - const selectedAgent = model?.includes('deep-cody') ? 'deep-cody' : undefined + const selectedAgent = model?.includes(DeepCodyAgentID) ? DeepCodyAgentID : undefined this.chatBuilder.addHumanMessage({ text: inputText, @@ -792,7 +796,7 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv } this.chatBuilder.setSelectedModel(model) - const chatAgent = model.includes('deep-cody') ? 'deep-cody' : undefined + const chatAgent = model.includes(DeepCodyAgentID) ? DeepCodyAgentID : undefined const recorder = await OmniboxTelemetry.create({ requestID, @@ -896,7 +900,11 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv // Now that we have the confirmation, proceed based on the user's choice - this.postViewTranscript({ speaker: 'assistant', processes: [step], model }) + this.postViewTranscript({ + speaker: 'assistant', + processes: [step], + model, + }) return confirmation }, postDone: (op?: { abort: boolean }): void => { diff --git a/vscode/src/chat/chat-view/handlers/registry.ts b/vscode/src/chat/chat-view/handlers/registry.ts index d6cc41696162..591d1ca076c6 100644 --- a/vscode/src/chat/chat-view/handlers/registry.ts +++ b/vscode/src/chat/chat-view/handlers/registry.ts @@ -1,6 +1,6 @@ import Anthropic from '@anthropic-ai/sdk' +import { DeepCodyAgentID } from '@sourcegraph/cody-shared/src/models/client' import { getConfiguration } from '../../../configuration' -import { DeepCodyAgent } from '../../agentic/DeepCody' import { ChatHandler } from './ChatHandler' import { DeepCodyHandler } from './DeepCodyHandler' import { EditHandler } from './EditHandler' @@ -20,7 +20,7 @@ function registerAgent(id: string, ctr: (id: string, tools: AgentTools) => Agent export function getAgent(id: string, modelId: string, tools: AgentTools): AgentHandler { const { contextRetriever, editor, chatClient } = tools - if (id === DeepCodyAgent.id) { + if (id === DeepCodyAgentID) { return new DeepCodyHandler(modelId, contextRetriever, editor, chatClient) } if (agentRegistry.has(id)) { diff --git a/vscode/src/chat/chat-view/prompt.ts b/vscode/src/chat/chat-view/prompt.ts index d528559cc553..2926f028a11b 100644 --- a/vscode/src/chat/chat-view/prompt.ts +++ b/vscode/src/chat/chat-view/prompt.ts @@ -8,9 +8,9 @@ import { isDefined, wrapInActiveSpan, } from '@sourcegraph/cody-shared' +import { DeepCodyAgentID } from '@sourcegraph/cody-shared/src/models/client' import { logDebug } from '../../output-channel-logger' import { PromptBuilder } from '../../prompt-builder' -import { DeepCodyAgent } from '../agentic/DeepCody' import { ChatBuilder } from './ChatBuilder' export interface PromptInfo { @@ -64,7 +64,7 @@ export class DefaultPrompter { .flatMap(m => (m.contextFiles ? [...m.contextFiles].reverse() : [])) .filter(isDefined) - const isContextAgentEnabled = reverseTranscript[0]?.agent === DeepCodyAgent.id + const isContextAgentEnabled = reverseTranscript[0]?.agent === DeepCodyAgentID // Apply the context preamble via the prompt mixin to the last open-ended human message that is not a command. // The context preamble provides additional instructions on how Cody should respond using the attached context items, diff --git a/vscode/webviews/chat/Transcript.tsx b/vscode/webviews/chat/Transcript.tsx index 56a28c481782..774e9393f9ec 100644 --- a/vscode/webviews/chat/Transcript.tsx +++ b/vscode/webviews/chat/Transcript.tsx @@ -50,6 +50,7 @@ import { import { HumanMessageCell } from './cells/messageCell/human/HumanMessageCell' import { type Context, type Span, context, trace } from '@opentelemetry/api' +import { DeepCodyAgentID } from '@sourcegraph/cody-shared/src/models/client' import { isCodeSearchContextItem } from '../../src/context/openctx/codeSearch' import { TELEMETRY_INTENT } from '../../src/telemetry/onebox' import { useIntentDetectionConfig } from '../components/omnibox/intentDetection' @@ -326,9 +327,15 @@ const TranscriptInteraction: FC = memo(props => { const { intent, intentScores, - }: { intent: ChatMessage['intent']; intentScores: IntentResults['allScores'] } = + }: { + intent: ChatMessage['intent'] + intentScores: IntentResults['allScores'] + } = query === intentResults?.query - ? { intent: intentResults.intent, intentScores: intentResults.allScores } + ? { + intent: intentResults.intent, + intentScores: intentResults.allScores, + } : { intent: undefined, intentScores: [] } const commonProps = { @@ -759,7 +766,7 @@ const TranscriptInteraction: FC = memo(props => { ? EditContextButtonSearch : EditContextButtonChat } - defaultOpen={isContextLoading && humanMessage.agent === 'deep-cody'} + defaultOpen={isContextLoading && humanMessage.agent === DeepCodyAgentID} processes={humanMessage?.processes ?? undefined} agent={humanMessage?.agent ?? undefined} /> diff --git a/vscode/webviews/chat/cells/contextCell/ContextCell.tsx b/vscode/webviews/chat/cells/contextCell/ContextCell.tsx index b8772c033066..707107262a9c 100644 --- a/vscode/webviews/chat/cells/contextCell/ContextCell.tsx +++ b/vscode/webviews/chat/cells/contextCell/ContextCell.tsx @@ -6,6 +6,7 @@ import type { RankedContext, } from '@sourcegraph/cody-shared' import { pluralize } from '@sourcegraph/cody-shared' +import { DeepCodyAgentID } from '@sourcegraph/cody-shared/src/models/client' import { MENTION_CLASS_NAME } from '@sourcegraph/prompt-editor' import { clsx } from 'clsx' import { BrainIcon, FilePenLine, MessagesSquareIcon } from 'lucide-react' @@ -27,7 +28,9 @@ import { Cell } from '../Cell' import { NON_HUMAN_CELL_AVATAR_SIZE } from '../messageCell/assistant/AssistantMessageCell' import styles from './ContextCell.module.css' -export const __ContextCellStorybookContext = createContext<{ initialOpen: boolean } | null>(null) +export const __ContextCellStorybookContext = createContext<{ + initialOpen: boolean +} | null>(null) /** * A component displaying the context for a human message. @@ -133,7 +136,7 @@ export const ContextCell: FunctionComponent<{ const telemetryRecorder = useTelemetryRecorder() - const isAgenticChat = model?.includes('deep-cody') || agent === 'deep-cody' + const isAgenticChat = model?.includes(DeepCodyAgentID) || agent === DeepCodyAgentID // Text for top header text const headerText: { main: string; sub?: string } = { diff --git a/vscode/webviews/chat/cells/messageCell/assistant/AssistantMessageCell.tsx b/vscode/webviews/chat/cells/messageCell/assistant/AssistantMessageCell.tsx index 6d8502e5c1f8..3b09866a1e1b 100644 --- a/vscode/webviews/chat/cells/messageCell/assistant/AssistantMessageCell.tsx +++ b/vscode/webviews/chat/cells/messageCell/assistant/AssistantMessageCell.tsx @@ -13,6 +13,7 @@ import { reformatBotMessageForChat, serializedPromptEditorStateFromChatMessage, } from '@sourcegraph/cody-shared' +import { DeepCodyAgentID } from '@sourcegraph/cody-shared/src/models/client' import type { PromptEditorRefAPI } from '@sourcegraph/prompt-editor' import isEqual from 'lodash/isEqual' import { type FunctionComponent, type RefObject, memo, useMemo } from 'react' @@ -103,7 +104,7 @@ export const AssistantMessageCell: FunctionComponent<{ isSearchIntent ? undefined : ( {chatModel - ? chatModel.id.includes('deep-cody') + ? chatModel.id.includes(DeepCodyAgentID) ? 'Claude 3.5 Sonnet (New)' : chatModel.title ?? `Model ${chatModel.id} by ${chatModel.provider}` : 'Model'} @@ -284,7 +285,7 @@ function useChatModelByID( (model ? { id: model, - title: model?.includes('deep-cody') ? 'Deep Cody (Experimental)' : model, + title: model?.includes(DeepCodyAgentID) ? 'Deep Cody (Experimental)' : model, provider: 'unknown', tags: [], } diff --git a/vscode/webviews/components/ChatModelIcon.tsx b/vscode/webviews/components/ChatModelIcon.tsx index 6f442c4dbca7..098ba009ce43 100644 --- a/vscode/webviews/components/ChatModelIcon.tsx +++ b/vscode/webviews/components/ChatModelIcon.tsx @@ -1,3 +1,4 @@ +import { DeepCodyAgentID } from '@sourcegraph/cody-shared/src/models/client' import type { FunctionComponent } from 'react' import { CodyLogoBW } from '../icons/CodyLogo' import { @@ -15,7 +16,7 @@ export function chatModelIconComponent( if (model.startsWith('openai') || model.includes('gpt')) { return OpenAILogo } - if (model.includes('anthropic') || model.includes('deep-cody')) { + if (model.includes('anthropic') || model.includes(DeepCodyAgentID)) { return AnthropicLogo } if (model.startsWith('google') || model.includes('gemini')) { diff --git a/vscode/webviews/components/modelSelectField/ModelSelectField.story.tsx b/vscode/webviews/components/modelSelectField/ModelSelectField.story.tsx index de6f3d3ae602..4a3b0d1f2a43 100644 --- a/vscode/webviews/components/modelSelectField/ModelSelectField.story.tsx +++ b/vscode/webviews/components/modelSelectField/ModelSelectField.story.tsx @@ -3,6 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react' import { VSCodeStandaloneComponent } from '../../storybook/VSCodeStoryDecorator' import { type Model, ModelTag, ModelUsage, getMockedDotComClientModels } from '@sourcegraph/cody-shared' +import { DeepCodyAgentID } from '@sourcegraph/cody-shared/src/models/client' import { useArgs } from '@storybook/preview-api' import { ModelSelectField } from './ModelSelectField' @@ -19,7 +20,7 @@ const MODELS: Model[] = [ { title: 'Deep Cody', provider: 'sourcegraph', - id: 'deep-cody', + id: DeepCodyAgentID, contextWindow: { input: 100, output: 100 }, usage: [ModelUsage.Chat], tags: [ModelTag.Pro, ModelTag.Experimental], diff --git a/vscode/webviews/components/modelSelectField/ModelSelectField.tsx b/vscode/webviews/components/modelSelectField/ModelSelectField.tsx index 300785cc6163..91c753b9fdf7 100644 --- a/vscode/webviews/components/modelSelectField/ModelSelectField.tsx +++ b/vscode/webviews/components/modelSelectField/ModelSelectField.tsx @@ -1,4 +1,5 @@ import { type Model, ModelTag, isCodyProModel, isWaitlistModel } from '@sourcegraph/cody-shared' +import { DeepCodyAgentID } from '@sourcegraph/cody-shared/src/models/client' import { clsx } from 'clsx' import { BookOpenIcon, BrainIcon, BuildingIcon, ExternalLinkIcon } from 'lucide-react' import { type FunctionComponent, type ReactNode, useCallback, useMemo } from 'react' @@ -303,7 +304,7 @@ function modelAvailability( } function getTooltip(model: Model, availability: string): string { - if (model.id.includes('deep-cody')) { + if (model.id.includes(DeepCodyAgentID)) { return 'Agentic chat reflects on your request and uses tools to dynamically retrieve relevant context, improving accuracy and response quality.' } @@ -356,9 +357,13 @@ const ModelTitleWithIcon: React.FC<{ const isDisabled = modelAvailability !== 'available' return ( - + {showIcon ? ( - model.id.includes('deep-cody') ? ( + model.id.includes(DeepCodyAgentID) ? ( ) : ( @@ -379,10 +384,10 @@ const ModelTitleWithIcon: React.FC<{ ) } -const ChatModelIcon: FunctionComponent<{ model: string; className?: string }> = ({ - model, - className, -}) => { +const ChatModelIcon: FunctionComponent<{ + model: string + className?: string +}> = ({ model, className }) => { const ModelIcon = chatModelIconComponent(model) return ModelIcon ? : null } @@ -398,7 +403,7 @@ const ModelUIGroup: Record = { } const getModelDropDownUIGroup = (model: Model): string => { - if (['deep-cody', 'tool-cody'].some(id => model.id.includes(id))) return ModelUIGroup.Agents + if ([DeepCodyAgentID, 'tool-cody'].some(id => model.id.includes(id))) return ModelUIGroup.Agents if (model.tags.includes(ModelTag.Power)) return ModelUIGroup.Power if (model.tags.includes(ModelTag.Balanced)) return ModelUIGroup.Balanced if (model.tags.includes(ModelTag.Speed)) return ModelUIGroup.Speed From 4d756e42f70ccc5ef1a9b244f7c10cc748d7c8e9 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 6 Feb 2025 13:12:28 -0800 Subject: [PATCH 02/10] experimental: Re-enable tool-cody for exploration (#6979) This fixes `tool-cody` not working anymore by making the `model` and `agentName` checks actually check for tool-cody. To test it, you need to set the experimental setting: ```json { "cody.experimental.minion.anthropicKey": "anthropic api key here", } ``` I also added a "run terminal command" tool that's by no means production ready but helped me test things easily and get more familiar with the codebase again. ## Test plan - Set `"cody.experimental.minion.anthropicKey": "anthropic api key here"` in VS Code settings - Open Cody, select `Tool Cody` that's now visible - Ask Cody: "Can you run `wc -l` against please?" - See it return the output --- lib/shared/src/models/client.ts | 7 +- lib/shared/src/models/sync.ts | 4 +- vscode/src/chat/chat-view/ChatController.ts | 16 +- .../chat/chat-view/handlers/ToolHandler.ts | 148 ++++++++++++++++++ .../src/chat/chat-view/handlers/registry.ts | 4 +- vscode/webviews/chat/Transcript.tsx | 14 +- .../modelSelectField/ModelSelectField.tsx | 5 +- 7 files changed, 182 insertions(+), 16 deletions(-) diff --git a/lib/shared/src/models/client.ts b/lib/shared/src/models/client.ts index 2b47ba6859dc..2dfea97950c8 100644 --- a/lib/shared/src/models/client.ts +++ b/lib/shared/src/models/client.ts @@ -35,10 +35,13 @@ function getDeepCodyServerModel(): ServerModel { } } +export const ToolCodyModelRef = 'sourcegraph::2024-12-31::tool-cody' +export const ToolCodyModelName = 'tool-cody' + export const TOOL_CODY_MODEL: ServerModel = { - modelRef: 'sourcegraph::2024-12-31::tool-cody', + modelRef: ToolCodyModelRef, displayName: 'Tool Cody', - modelName: 'tool-cody', + modelName: ToolCodyModelName, capabilities: ['chat'], category: 'accuracy', status: 'internal' as ModelTag.Internal, diff --git a/lib/shared/src/models/sync.ts b/lib/shared/src/models/sync.ts index 3d0796c67337..e53136ffd3f8 100644 --- a/lib/shared/src/models/sync.ts +++ b/lib/shared/src/models/sync.ts @@ -26,7 +26,7 @@ import { RestClient } from '../sourcegraph-api/rest/client' import type { UserProductSubscription } from '../sourcegraph-api/userProductSubscription' import { CHAT_INPUT_TOKEN_BUDGET } from '../token/constants' import { isError } from '../utils' -import { TOOL_CODY_MODEL, getExperimentalClientModelByFeatureFlag } from './client' +import { TOOL_CODY_MODEL, ToolCodyModelName, getExperimentalClientModelByFeatureFlag } from './client' import { type Model, type ServerModel, createModel, createModelFromServerModel } from './model' import type { DefaultsAndUserPreferencesForEndpoint, @@ -265,7 +265,7 @@ export function syncModels({ } const hasToolCody = data.primaryModels.some(m => - m.id.includes('tool-cody') + m.id.includes(ToolCodyModelName) ) if (!hasToolCody && isToolCodyEnabled) { clientModels.push(TOOL_CODY_MODEL) diff --git a/vscode/src/chat/chat-view/ChatController.ts b/vscode/src/chat/chat-view/ChatController.ts index 10298dcae8fb..1837af8a2334 100644 --- a/vscode/src/chat/chat-view/ChatController.ts +++ b/vscode/src/chat/chat-view/ChatController.ts @@ -73,7 +73,11 @@ import { type Span, context } from '@opentelemetry/api' import { captureException } from '@sentry/core' import type { SubMessage } from '@sourcegraph/cody-shared/src/chat/transcript/messages' import { resolveAuth } from '@sourcegraph/cody-shared/src/configuration/auth-resolver' -import { DeepCodyAgentID } from '@sourcegraph/cody-shared/src/models/client' +import { + DeepCodyAgentID, + ToolCodyModelName, + ToolCodyModelRef, +} from '@sourcegraph/cody-shared/src/models/client' import type { TelemetryEventParameters } from '@sourcegraph/telemetry' import { Subject, map } from 'observable-fns' import type { URI } from 'vscode-uri' @@ -671,7 +675,10 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv firstResultFromOperation(ChatBuilder.resolvedModelForChat(this.chatBuilder)) ) this.chatBuilder.setSelectedModel(model) - const selectedAgent = model?.includes(DeepCodyAgentID) ? DeepCodyAgentID : undefined + let selectedAgent = model?.includes(DeepCodyAgentID) ? DeepCodyAgentID : undefined + if (model?.includes(ToolCodyModelName)) { + selectedAgent = ToolCodyModelRef + } this.chatBuilder.addHumanMessage({ text: inputText, @@ -796,7 +803,10 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv } this.chatBuilder.setSelectedModel(model) - const chatAgent = model.includes(DeepCodyAgentID) ? DeepCodyAgentID : undefined + let chatAgent = model.includes(DeepCodyAgentID) ? DeepCodyAgentID : undefined + if (model.includes(ToolCodyModelName)) { + chatAgent = ToolCodyModelRef + } const recorder = await OmniboxTelemetry.create({ requestID, diff --git a/vscode/src/chat/chat-view/handlers/ToolHandler.ts b/vscode/src/chat/chat-view/handlers/ToolHandler.ts index aa3ae0a47468..ce819f16005f 100644 --- a/vscode/src/chat/chat-view/handlers/ToolHandler.ts +++ b/vscode/src/chat/chat-view/handlers/ToolHandler.ts @@ -1,3 +1,5 @@ +import { spawn } from 'node:child_process' +import type { SpawnOptions } from 'node:child_process' import type Anthropic from '@anthropic-ai/sdk' import type { ContentBlock, MessageParam, Tool, ToolResultBlockParam } from '@anthropic-ai/sdk/resources' import { ProcessType, PromptString } from '@sourcegraph/cody-shared' @@ -52,6 +54,44 @@ const allTools: CodyTool[] = [ } }, }, + { + spec: { + name: 'run_terminal_command', + description: 'Run an arbitrary terminal command at the root of the users project. ', + input_schema: { + type: 'object', + properties: { + command: { + type: 'string', + description: + 'The command to run in the root of the users project. Must be shell escaped.', + }, + }, + required: ['command'], + }, + }, + invoke: async (input: { command: string }) => { + if (typeof input.command !== 'string') { + throw new Error( + `run_terminal_command argument must be a string, value was ${JSON.stringify(input)}` + ) + } + + const workspaceFolder = vscode.workspace.workspaceFolders?.[0] + if (!workspaceFolder) { + throw new Error('No workspace folder found') + } + + try { + const commandResult = await runShellCommand(input.command, { + cwd: workspaceFolder.uri.path, + }) + return commandResult.stdout + } catch (error) { + throw new Error(`Failed to run terminal command: ${input.command}: ${error}`) + } + }, + }, ] export class ExperimentalToolHandler implements AgentHandler { @@ -169,3 +209,111 @@ export class ExperimentalToolHandler implements AgentHandler { delegate.postDone() } } + +interface CommandOptions { + cwd?: string + env?: Record +} + +interface CommandResult { + stdout: string + stderr: string + code: number | null + signal: NodeJS.Signals | null +} + +class CommandError extends Error { + constructor( + message: string, + public readonly result: CommandResult + ) { + super(message) + this.name = 'CommandError' + } +} + +async function runShellCommand(command: string, options: CommandOptions = {}): Promise { + const { cwd = process.cwd(), env = process.env } = options + + const timeout = 10_000 + const maxBuffer = 1024 * 1024 * 10 + const encoding = 'utf8' + const spawnOptions: SpawnOptions = { + shell: true, + cwd, + env, + windowsHide: true, + } + + return new Promise((resolve, reject) => { + const process = spawn(command, [], spawnOptions) + + let stdout = '' + let stderr = '' + let killed = false + const timeoutId = setTimeout(() => { + killed = true + process.kill() + reject(new Error(`Command timed out after ${timeout}ms`)) + }, timeout) + + let stdoutLength = 0 + let stderrLength = 0 + + if (process.stdout) { + process.stdout.on('data', (data: Buffer) => { + const chunk = data.toString(encoding) + stdoutLength += chunk.length + if (stdoutLength > maxBuffer) { + killed = true + process.kill() + reject(new Error('stdout maxBuffer exceeded')) + return + } + stdout += chunk + }) + } + + if (process.stderr) { + process.stderr.on('data', (data: Buffer) => { + const chunk = data.toString(encoding) + stderrLength += chunk.length + if (stderrLength > maxBuffer) { + killed = true + process.kill() + reject(new Error('stderr maxBuffer exceeded')) + return + } + stderr += chunk + }) + } + + process.on('error', (error: Error) => { + if (timeoutId) clearTimeout(timeoutId) + reject(new Error(`Failed to start process: ${error.message}`)) + }) + + process.on('close', (code: number | null, signal: NodeJS.Signals | null) => { + if (timeoutId) clearTimeout(timeoutId) + if (killed) return + + const result: CommandResult = { + stdout, + stderr, + code, + signal, + } + + if (code === 0) { + resolve(result) + } else { + reject( + new CommandError( + `Command failed with exit code ${code}${stderr ? ': ' + stderr : ''}`, + result + ) + ) + } + }) + }) +} diff --git a/vscode/src/chat/chat-view/handlers/registry.ts b/vscode/src/chat/chat-view/handlers/registry.ts index 591d1ca076c6..93c11a84362f 100644 --- a/vscode/src/chat/chat-view/handlers/registry.ts +++ b/vscode/src/chat/chat-view/handlers/registry.ts @@ -1,5 +1,5 @@ import Anthropic from '@anthropic-ai/sdk' -import { DeepCodyAgentID } from '@sourcegraph/cody-shared/src/models/client' +import { DeepCodyAgentID, ToolCodyModelRef } from '@sourcegraph/cody-shared/src/models/client' import { getConfiguration } from '../../../configuration' import { ChatHandler } from './ChatHandler' import { DeepCodyHandler } from './DeepCodyHandler' @@ -41,7 +41,7 @@ registerAgent( (_id: string, { contextRetriever, editor }: AgentTools) => new EditHandler('insert', contextRetriever, editor) ) -registerAgent('sourcegraph::2024-12-31::tool-cody', (_id: string) => { +registerAgent(ToolCodyModelRef, (_id: string) => { const config = getConfiguration() const anthropicAPI = new Anthropic({ apiKey: config.experimentalMinionAnthropicKey, diff --git a/vscode/webviews/chat/Transcript.tsx b/vscode/webviews/chat/Transcript.tsx index 774e9393f9ec..59d06a15aa35 100644 --- a/vscode/webviews/chat/Transcript.tsx +++ b/vscode/webviews/chat/Transcript.tsx @@ -50,7 +50,7 @@ import { import { HumanMessageCell } from './cells/messageCell/human/HumanMessageCell' import { type Context, type Span, context, trace } from '@opentelemetry/api' -import { DeepCodyAgentID } from '@sourcegraph/cody-shared/src/models/client' +import { DeepCodyAgentID, ToolCodyModelName } from '@sourcegraph/cody-shared/src/models/client' import { isCodeSearchContextItem } from '../../src/context/openctx/codeSearch' import { TELEMETRY_INTENT } from '../../src/telemetry/onebox' import { useIntentDetectionConfig } from '../components/omnibox/intentDetection' @@ -291,7 +291,9 @@ const TranscriptInteraction: FC = memo(props => { const lastEditorRef = useContext(LastEditorContext) useImperativeHandle(parentEditorRef, () => humanEditorRef.current) - const { doIntentDetection } = useIntentDetectionConfig() + const usingToolCody = assistantMessage?.model?.includes(ToolCodyModelName) + const doIntentDetection = useIntentDetectionConfig().doIntentDetection && !usingToolCody + const onUserAction = useCallback( (action: 'edit' | 'submit', intentFromSubmit?: ChatMessage['intent']) => { // Start the span as soon as the user initiates the action @@ -396,7 +398,7 @@ const TranscriptInteraction: FC = memo(props => { ) const extensionAPI = useExtensionAPI() - const experimentalOneBoxEnabled = useOmniBox() + const experimentalOneBoxEnabled = useOmniBox() && !usingToolCody const prefetchIntent = useMemo(() => { const handler = async (editorValue: SerializedPromptEditorValue) => { @@ -689,6 +691,7 @@ const TranscriptInteraction: FC = memo(props => { }), [humanMessage] ) + return ( <> = memo(props => { switchToSearch={() => editAndSubmitSearch(assistantMessage?.didYouMeanQuery ?? '')} /> )} - {!isSearchIntent && humanMessage.agent && ( + {!usingToolCody && !isSearchIntent && humanMessage.agent && ( = memo(props => { isContextLoading && assistantMessage?.isLoading && } - {!(humanMessage.agent && isContextLoading) && + {!usingToolCody && + !(humanMessage.agent && isContextLoading) && (humanMessage.contextFiles || assistantMessage || isContextLoading) && !isSearchIntent && ( = { } const getModelDropDownUIGroup = (model: Model): string => { - if ([DeepCodyAgentID, 'tool-cody'].some(id => model.id.includes(id))) return ModelUIGroup.Agents + if ([DeepCodyAgentID, ToolCodyModelName].some(id => model.id.includes(id))) + return ModelUIGroup.Agents if (model.tags.includes(ModelTag.Power)) return ModelUIGroup.Power if (model.tags.includes(ModelTag.Balanced)) return ModelUIGroup.Balanced if (model.tags.includes(ModelTag.Speed)) return ModelUIGroup.Speed From 127d52ca349759e31cefb3dcde0fe5a505ca4ac6 Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 6 Feb 2025 13:12:36 -0800 Subject: [PATCH 03/10] chat ui: remove user avatars, remove speaker name from follow-up message (#6981) The user knows who they are, why show it next to every message? It clutters the UI. So I removed the avatar from the human messages (we still have the avatar in the top right). I also removed the name from the follow-up message. It doesn't add anything. Ideally we'd remove the "user name" every where but when I did that, the layout looked kinda bad because there was still the 'split into chat' button. ## Before ![before](https://github.com/user-attachments/assets/2ce844e1-275a-42e4-876c-5d18bb011a47) ## After ![after](https://github.com/user-attachments/assets/364a23a7-242b-4ed7-a49f-08de8df6b47f) ## Test plan - N/A --- .../cells/messageCell/human/HumanMessageCell.tsx | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/vscode/webviews/chat/cells/messageCell/human/HumanMessageCell.tsx b/vscode/webviews/chat/cells/messageCell/human/HumanMessageCell.tsx index 2aef502a4fc4..0059e86adca4 100644 --- a/vscode/webviews/chat/cells/messageCell/human/HumanMessageCell.tsx +++ b/vscode/webviews/chat/cells/messageCell/human/HumanMessageCell.tsx @@ -10,8 +10,7 @@ import isEqual from 'lodash/isEqual' import { ColumnsIcon } from 'lucide-react' import { type FC, memo, useMemo } from 'react' import type { UserAccountInfo } from '../../../../Chat' -import { UserAvatar } from '../../../../components/UserAvatar' -import { BaseMessageCell, MESSAGE_CELL_AVATAR_SIZE } from '../BaseMessageCell' +import { BaseMessageCell } from '../BaseMessageCell' import { HumanMessageEditor } from './editor/HumanMessageEditor' import { Tooltip, TooltipContent, TooltipTrigger } from '../../../../components/shadcn/ui/tooltip' @@ -94,14 +93,7 @@ const HumanMessageCellContent = memo(props => { return ( - } - speakerTitle={userInfo.user.displayName ?? userInfo.user.username} + speakerTitle={isFirstMessage && (userInfo.user.displayName ?? userInfo.user.username)} cellAction={
{isFirstMessage && } From 9c9670e6c1503349fd0c5d4822e62bd059713565 Mon Sep 17 00:00:00 2001 From: Quinn Slack Date: Thu, 6 Feb 2025 16:28:42 -0800 Subject: [PATCH 04/10] remove chat message feedback and "Try again with different" model/context (#6980) These are noisy in the UI and are not needed anymore. - We have other ways of gathering feedback from internal evals and from tracking more end-to-end metrics, and thumbs up/down feedback was low quality anyway. - The "Try again" functionality is used by a small % of users (<5%) and has not been mentioned as helpful by any users in our feedback.

Removed:

![image](https://github.com/user-attachments/assets/5110b689-69b9-4011-a3b8-221a73c3001f) ## Test plan CI ## Changelog - Removed the thumbs up/down feedback and "Try again with different" model/context options from chat because they were infrequently used. --- vscode/test/e2e/initial-context.test.ts | 45 ------ vscode/webviews/Chat.tsx | 38 +---- vscode/webviews/chat/Transcript.story.tsx | 1 - vscode/webviews/chat/Transcript.test.tsx | 2 - vscode/webviews/chat/Transcript.tsx | 10 -- .../assistant/AssistantMessageCell.tsx | 30 ---- .../assistant/ContextFocusActions.tsx | 147 ------------------ .../messageCell/assistant/SearchResults.tsx | 8 - .../components/FeedbackButtons.module.css | 18 --- .../chat/components/FeedbackButtons.tsx | 88 ----------- 10 files changed, 1 insertion(+), 386 deletions(-) delete mode 100644 vscode/webviews/chat/cells/messageCell/assistant/ContextFocusActions.tsx delete mode 100644 vscode/webviews/chat/components/FeedbackButtons.module.css delete mode 100644 vscode/webviews/chat/components/FeedbackButtons.tsx diff --git a/vscode/test/e2e/initial-context.test.ts b/vscode/test/e2e/initial-context.test.ts index 4a9bfc5ad84f..1c7569032151 100644 --- a/vscode/test/e2e/initial-context.test.ts +++ b/vscode/test/e2e/initial-context.test.ts @@ -79,48 +79,3 @@ testWithGitRemote('initial context - file', async ({ page, sidebar, server }) => 'host.example/user/myrepo', ]) }) - -testWithGitRemote.extend({ dotcomUrl: mockServer.SERVER_URL })( - 'chat try-again actions', - async ({ page, sidebar }) => { - await sidebarSignin(page, sidebar) - await openFileInEditorTab(page, 'main.c') - - const chatPanel = getChatSidebarPanel(page) - const firstChatInput = getChatInputs(chatPanel).first() - await expect(chatInputMentions(firstChatInput)).toHaveText(['main.c', 'myrepo']) - await firstChatInput.pressSequentially('xyz') - await firstChatInput.press('Enter') - - const contextFocusActions = chatPanel.getByRole('group', { - name: 'Try again with different context', - }) - await expect(contextFocusActions).toBeVisible() - await expect(contextFocusActions.getByRole('button')).toHaveText([ - 'Public knowledge only', - 'Current file only', - 'Add context...', - ]) - - const currentFileOnlyButton = contextFocusActions.getByRole('button', { - name: 'Current file only', - }) - await currentFileOnlyButton.click() - await expect(chatInputMentions(firstChatInput)).toHaveText(['main.c']) - await expect(firstChatInput).toHaveText('main.c xyz') - - const publicKnowledgeOnlyButton = contextFocusActions.getByRole('button', { - name: 'Public knowledge only', - }) - await publicKnowledgeOnlyButton.click() - await expect(chatInputMentions(firstChatInput)).toHaveCount(0) - await expect(firstChatInput).toHaveText('xyz') - - const addContextButton = contextFocusActions.getByRole('button', { - name: 'Add context...', - }) - await addContextButton.click() - await expect(firstChatInput).toBeFocused() - await expect(firstChatInput).toHaveText('xyz @') - } -) diff --git a/vscode/webviews/Chat.tsx b/vscode/webviews/Chat.tsx index fc53c93e576b..78465faf94d2 100644 --- a/vscode/webviews/Chat.tsx +++ b/vscode/webviews/Chat.tsx @@ -13,8 +13,6 @@ import { Transcript, focusLastHumanMessageEditor } from './chat/Transcript' import type { VSCodeWrapper } from './utils/VSCodeApi' import type { Context } from '@opentelemetry/api' -import { truncateTextStart } from '@sourcegraph/cody-shared/src/prompt/truncation' -import { CHAT_INPUT_TOKEN_BUDGET } from '@sourcegraph/cody-shared/src/token/constants' import styles from './Chat.module.css' import WelcomeFooter from './chat/components/WelcomeFooter' import { WelcomeMessage } from './chat/components/WelcomeMessage' @@ -22,7 +20,7 @@ import { WelcomeNotice } from './chat/components/WelcomeNotice' import { ScrollDown } from './components/ScrollDown' import type { View } from './tabs' import { SpanManager } from './utils/spanManager' -import { getTraceparentFromSpanContext, useTelemetryRecorder } from './utils/telemetry' +import { getTraceparentFromSpanContext } from './utils/telemetry' import { useUserAccountInfo } from './utils/useConfig' interface ChatboxProps { chatEnabled: boolean @@ -55,44 +53,11 @@ export const Chat: React.FunctionComponent isPromptsV2Enabled, isWorkspacesUpgradeCtaEnabled, }) => { - const telemetryRecorder = useTelemetryRecorder() - const transcriptRef = useRef(transcript) transcriptRef.current = transcript const userInfo = useUserAccountInfo() - const feedbackButtonsOnSubmit = useCallback( - (text: string) => { - enum FeedbackType { - thumbsUp = 1, - thumbsDown = 0, - } - - telemetryRecorder.recordEvent('cody.feedback', 'submit', { - metadata: { - feedbackType: text === 'thumbsUp' ? FeedbackType.thumbsUp : FeedbackType.thumbsDown, - recordsPrivateMetadataTranscript: userInfo.isDotComUser ? 1 : 0, - }, - privateMetadata: { - FeedbackText: text, - - // 🚨 SECURITY: chat transcripts are to be included only for DotCom users AND for V2 telemetry - // V2 telemetry exports privateMetadata only for DotCom users - // the condition below is an aditional safegaurd measure - responseText: userInfo.isDotComUser - ? truncateTextStart(transcriptRef.current.toString(), CHAT_INPUT_TOKEN_BUDGET) - : '', - }, - billingMetadata: { - product: 'cody', - category: 'billable', - }, - }) - }, - [userInfo, telemetryRecorder] - ) - const copyButtonOnSubmit = useCallback( (text: string, eventType: 'Button' | 'Keydown' = 'Button') => { const op = 'copy' @@ -243,7 +208,6 @@ export const Chat: React.FunctionComponent transcript={transcript} models={models} messageInProgress={messageInProgress} - feedbackButtonsOnSubmit={feedbackButtonsOnSubmit} copyButtonOnSubmit={copyButtonOnSubmit} insertButtonOnSubmit={insertButtonOnSubmit} smartApply={smartApply} diff --git a/vscode/webviews/chat/Transcript.story.tsx b/vscode/webviews/chat/Transcript.story.tsx index e6b72e119700..7b53dad7691f 100644 --- a/vscode/webviews/chat/Transcript.story.tsx +++ b/vscode/webviews/chat/Transcript.story.tsx @@ -37,7 +37,6 @@ const meta: Meta = { args: { transcript: FIXTURE_TRANSCRIPT.simple, messageInProgress: null, - feedbackButtonsOnSubmit: () => {}, copyButtonOnSubmit: () => {}, insertButtonOnSubmit: () => {}, userInfo: FIXTURE_USER_ACCOUNT_INFO, diff --git a/vscode/webviews/chat/Transcript.test.tsx b/vscode/webviews/chat/Transcript.test.tsx index 731682fde793..5dc4b3728af5 100644 --- a/vscode/webviews/chat/Transcript.test.tsx +++ b/vscode/webviews/chat/Transcript.test.tsx @@ -16,7 +16,6 @@ const MOCK_MODELS = getMockedDotComClientModels() const PROPS: Omit, 'transcript'> = { messageInProgress: null, - feedbackButtonsOnSubmit: () => {}, copyButtonOnSubmit: () => {}, insertButtonOnSubmit: () => {}, userInfo: FIXTURE_USER_ACCOUNT_INFO, @@ -253,7 +252,6 @@ describe('Transcript', () => { { context: {} }, { message: 'Model\n\nRequest Failed: some error' }, ]) - expect(screen.queryByText('Try again with different context')).toBeNull() }) test('does not clobber user input into followup while isPendingPriorResponse when it completes', async () => { diff --git a/vscode/webviews/chat/Transcript.tsx b/vscode/webviews/chat/Transcript.tsx index 59d06a15aa35..048f368399f6 100644 --- a/vscode/webviews/chat/Transcript.tsx +++ b/vscode/webviews/chat/Transcript.tsx @@ -71,7 +71,6 @@ interface TranscriptProps { guardrails?: Guardrails postMessage?: ApiPostMessage - feedbackButtonsOnSubmit: (text: string) => void copyButtonOnSubmit: CodeBlockActionsProps['copyButtonOnSubmit'] insertButtonOnSubmit?: CodeBlockActionsProps['insertButtonOnSubmit'] smartApply?: CodeBlockActionsProps['smartApply'] @@ -89,7 +88,6 @@ export const Transcript: FC = props => { messageInProgress, guardrails, postMessage, - feedbackButtonsOnSubmit, copyButtonOnSubmit, insertButtonOnSubmit, smartApply, @@ -154,7 +152,6 @@ export const Transcript: FC = props => { interaction={interaction} guardrails={guardrails} postMessage={postMessage} - feedbackButtonsOnSubmit={feedbackButtonsOnSubmit} copyButtonOnSubmit={copyButtonOnSubmit} insertButtonOnSubmit={insertButtonOnSubmit} isFirstInteraction={i === 0} @@ -268,7 +265,6 @@ const TranscriptInteraction: FC = memo(props => { priorAssistantMessageIsLoading, userInfo, chatEnabled, - feedbackButtonsOnSubmit, postMessage, guardrails, insertButtonOnSubmit, @@ -784,18 +780,12 @@ const TranscriptInteraction: FC = memo(props => { models={models} chatEnabled={chatEnabled} message={assistantMessage} - feedbackButtonsOnSubmit={feedbackButtonsOnSubmit} copyButtonOnSubmit={copyButtonOnSubmit} insertButtonOnSubmit={insertButtonOnSubmit} postMessage={postMessage} guardrails={guardrails} humanMessage={humanMessageInfo} isLoading={assistantMessage.isLoading} - showFeedbackButtons={ - !assistantMessage.isLoading && - !assistantMessage.error && - isLastSentInteraction - } smartApply={smartApply} smartApplyEnabled={smartApplyEnabled} onSelectedFiltersUpdate={onSelectedFiltersUpdate} diff --git a/vscode/webviews/chat/cells/messageCell/assistant/AssistantMessageCell.tsx b/vscode/webviews/chat/cells/messageCell/assistant/AssistantMessageCell.tsx index 3b09866a1e1b..a59ea5c7d724 100644 --- a/vscode/webviews/chat/cells/messageCell/assistant/AssistantMessageCell.tsx +++ b/vscode/webviews/chat/cells/messageCell/assistant/AssistantMessageCell.tsx @@ -26,10 +26,8 @@ import { } from '../../../ChatMessageContent/ChatMessageContent' import { ErrorItem, RequestErrorItem } from '../../../ErrorItem' import { type Interaction, editHumanMessage } from '../../../Transcript' -import { FeedbackButtons } from '../../../components/FeedbackButtons' import { LoadingDots } from '../../../components/LoadingDots' import { BaseMessageCell, MESSAGE_CELL_AVATAR_SIZE } from '../BaseMessageCell' -import { ContextFocusActions } from './ContextFocusActions' import { SearchResults } from './SearchResults' import { SubMessageCell } from './SubMessageCell' @@ -46,9 +44,6 @@ export const AssistantMessageCell: FunctionComponent<{ chatEnabled: boolean isLoading: boolean - showFeedbackButtons: boolean - feedbackButtonsOnSubmit?: (text: string) => void - copyButtonOnSubmit?: CodeBlockActionsProps['copyButtonOnSubmit'] insertButtonOnSubmit?: CodeBlockActionsProps['insertButtonOnSubmit'] @@ -67,8 +62,6 @@ export const AssistantMessageCell: FunctionComponent<{ userInfo, chatEnabled, isLoading, - showFeedbackButtons, - feedbackButtonsOnSubmit, copyButtonOnSubmit, insertButtonOnSubmit, postMessage, @@ -129,8 +122,6 @@ export const AssistantMessageCell: FunctionComponent<{ )} @@ -176,27 +167,6 @@ export const AssistantMessageCell: FunctionComponent<{ Output stream stopped
)} -
- {showFeedbackButtons && - feedbackButtonsOnSubmit && - !(experimentalOneBoxEnabled && isSearchIntent) && ( - - )} - {!isLoading && (!message.error || isAborted) && !isSearchIntent && ( - - )} -
) } diff --git a/vscode/webviews/chat/cells/messageCell/assistant/ContextFocusActions.tsx b/vscode/webviews/chat/cells/messageCell/assistant/ContextFocusActions.tsx deleted file mode 100644 index cd6ef8c0fa48..000000000000 --- a/vscode/webviews/chat/cells/messageCell/assistant/ContextFocusActions.tsx +++ /dev/null @@ -1,147 +0,0 @@ -import { isDefined } from '@sourcegraph/cody-shared' -import clsx from 'clsx' -import { type FunctionComponent, useCallback, useMemo } from 'react' -import { Button } from '../../../../components/shadcn/ui/button' -import { Tooltip, TooltipContent, TooltipTrigger } from '../../../../components/shadcn/ui/tooltip' -import { getVSCodeAPI } from '../../../../utils/VSCodeApi' -import { useTelemetryRecorder } from '../../../../utils/telemetry' -import type { - HumanMessageInitialContextInfo as InitialContextInfo, - PriorHumanMessageInfo, -} from './AssistantMessageCell' - -export const ContextFocusActions: FunctionComponent<{ - humanMessage: PriorHumanMessageInfo - longResponseTime?: boolean - className?: string -}> = ({ humanMessage, longResponseTime, className }) => { - const telemetryRecorder = useTelemetryRecorder() - - const initialContextEventMetadata: Record = { - hasInitialContextRepositories: humanMessage.hasInitialContext.repositories ? 1 : 0, - hasInitialContextFiles: humanMessage.hasInitialContext.files ? 1 : 0, - } - - const logRerunWithDifferentContext = useCallback( - (rerunWith: InitialContextInfo): void => { - telemetryRecorder.recordEvent('cody.contextSelection', 'rerunWithDifferentContext', { - metadata: { - ...initialContextEventMetadata, - rerunWithInitialContextRepositories: rerunWith.repositories ? 1 : 0, - rerunWithInitialContextFiles: rerunWith.files ? 1 : 0, - }, - billingMetadata: { - product: 'cody', - category: 'billable', - }, - }) - }, - [telemetryRecorder, initialContextEventMetadata] - ) - - const actions = useMemo( - () => - ( - [ - humanMessage.hasInitialContext.repositories || humanMessage.hasInitialContext.files - ? { - label: 'Public knowledge only', - tooltip: 'Try again without context about your code', - onClick: () => { - const options: InitialContextInfo = { - repositories: false, - files: false, - } - logRerunWithDifferentContext(options) - humanMessage.rerunWithDifferentContext(options) - }, - } - : null, - humanMessage.hasInitialContext.repositories && humanMessage.hasInitialContext.files - ? { - label: 'Current file only', - tooltip: 'Try again, focused on the current file', - onClick: () => { - const options: InitialContextInfo = { - repositories: false, - files: true, - } - logRerunWithDifferentContext(options) - humanMessage.rerunWithDifferentContext(options) - }, - } - : null, - longResponseTime - ? { - label: 'Try again with a different model', - tooltip: - 'A new window will open with a copy of the current conversation where you can resubmit your request with a different model', - onClick: () => { - getVSCodeAPI().postMessage({ - command: 'chatSession', - action: 'duplicate', - }) - }, - } - : { - label: 'Add context...', - tooltip: 'Add relevant content to improve the response', - onClick: () => { - telemetryRecorder.recordEvent('cody.contextSelection', 'addFile', { - metadata: initialContextEventMetadata, - billingMetadata: { - product: 'cody', - category: 'core', - }, - }) - humanMessage.appendAtMention() - }, - }, - ] as { label: string; tooltip: string; onClick: () => void }[] - ) - .flat() - .filter(isDefined), - [ - humanMessage, - telemetryRecorder, - logRerunWithDifferentContext, - initialContextEventMetadata, - longResponseTime, - ] - ) - return actions.length > 0 ? ( - -
- {!longResponseTime && ( -

- Try again with different context -

- )} -
    - {actions.map(({ label, tooltip, onClick }) => ( -
  • - - - - - {tooltip} - -
  • - ))} -
-
-
- ) : null -} diff --git a/vscode/webviews/chat/cells/messageCell/assistant/SearchResults.tsx b/vscode/webviews/chat/cells/messageCell/assistant/SearchResults.tsx index 4d0a091865b4..d32b58aea11e 100644 --- a/vscode/webviews/chat/cells/messageCell/assistant/SearchResults.tsx +++ b/vscode/webviews/chat/cells/messageCell/assistant/SearchResults.tsx @@ -17,7 +17,6 @@ import { Button } from '../../../../components/shadcn/ui/button' import { Label } from '../../../../components/shadcn/ui/label' import { useTelemetryRecorder } from '../../../../utils/telemetry' import { useOmniBoxDebug } from '../../../../utils/useOmniBox' -import { FeedbackButtons } from '../../../components/FeedbackButtons' import { InfoMessage } from '../../../components/InfoMessage' import { LoadingDots } from '../../../components/LoadingDots' import { SearchFilters } from './SearchFilters' @@ -27,8 +26,6 @@ import styles from './SearchResults.module.css' interface SearchResultsProps { message: ChatMessageWithSearch - showFeedbackButtons?: boolean - feedbackButtonsOnSubmit?: (text: string) => void onSelectedFiltersUpdate: (filters: NLSSearchDynamicFilter[]) => void /** * Whether or not search results can be selected as context for the next interaction. @@ -40,8 +37,6 @@ const DEFAULT_RESULTS_LIMIT = 10 export const SearchResults = ({ message, onSelectedFiltersUpdate, - showFeedbackButtons, - feedbackButtonsOnSubmit, enableContextSelection, }: SearchResultsProps) => { const telemetryRecorder = useTelemetryRecorder() @@ -404,9 +399,6 @@ export const SearchResults = ({ More results )} - {showFeedbackButtons && feedbackButtonsOnSubmit && ( - - )} )} diff --git a/vscode/webviews/chat/components/FeedbackButtons.module.css b/vscode/webviews/chat/components/FeedbackButtons.module.css deleted file mode 100644 index 0d2a823635e4..000000000000 --- a/vscode/webviews/chat/components/FeedbackButtons.module.css +++ /dev/null @@ -1,18 +0,0 @@ -.feedback-buttons { - display: flex; - flex-direction: row; -} - -.feedback-button[disabled] { - /* VSCodeButton's default cursor is not-allowed, but that's different to - native VS Code and feels off, especially when it quickly changes to - not-allowed after you submit feedback. So we reset it back to the - default cursor to fit in nicer with standard VS Code native behaviour */ - cursor: default; -} - -.thumbs-down-feedback-container { - display: flex; - align-items: center; - gap: calc(var(--spacing) / 4); -} diff --git a/vscode/webviews/chat/components/FeedbackButtons.tsx b/vscode/webviews/chat/components/FeedbackButtons.tsx deleted file mode 100644 index 0bb66c749cd3..000000000000 --- a/vscode/webviews/chat/components/FeedbackButtons.tsx +++ /dev/null @@ -1,88 +0,0 @@ -import { clsx } from 'clsx' -import { useCallback, useState } from 'react' -import { CODY_FEEDBACK_URL } from '../../../src/chat/protocol' -import { Button } from '../../components/shadcn/ui/button' -import styles from './FeedbackButtons.module.css' - -interface FeedbackButtonsProps { - className?: string - disabled?: boolean - feedbackButtonsOnSubmit: (text: string) => void -} - -export const FeedbackButtons: React.FunctionComponent = ({ - className, - feedbackButtonsOnSubmit, -}) => { - const [feedbackSubmitted, setFeedbackSubmitted] = useState('') - - const onFeedbackBtnSubmit = useCallback( - (text: string) => { - setFeedbackSubmitted(text) - feedbackButtonsOnSubmit(text) - }, - [feedbackButtonsOnSubmit] - ) - - return ( -
- {!feedbackSubmitted && ( - <> - - - - )} - {feedbackSubmitted === 'thumbsUp' && ( - - )} - {feedbackSubmitted === 'thumbsDown' && ( - - - - Give Feedback - - - )} -
- ) -} From aa9c0bcf1dc3bf33eb7be43ba65e0d99bae4be6d Mon Sep 17 00:00:00 2001 From: Thorsten Ball Date: Thu, 6 Feb 2025 19:00:12 -0800 Subject: [PATCH 05/10] bug fix: Fix 500ms delay when submitting chat message (#6990) This removes the 500ms delay when submitting messages. It was introduced in #6720. If I understood the PR correctly, it was introduced to fix the error message that shows up when trying to switch intent while a response is streaming in. But instead of waiting 500ms before aborting, we go back to aborting, and explicitly abort a possible ongoing request before editing the message and resubmitting it. In my testing, it fixes the bug. I also re-added the tests that were commented out in #6720. ## Test plan - Submit a message to Claude, saying something like "write me a long poem" so that it generates a lot of text - While Claude is generating, switch the intent manually by clicking on the button - No error message should pop up --- vscode/src/chat/chat-view/ChatController.ts | 16 +-- vscode/test/e2e/chat-context.test.ts | 2 +- vscode/test/e2e/chat-input.test.ts | 139 ++++++++++---------- 3 files changed, 77 insertions(+), 80 deletions(-) diff --git a/vscode/src/chat/chat-view/ChatController.ts b/vscode/src/chat/chat-view/ChatController.ts index 1837af8a2334..03af9e7aacce 100644 --- a/vscode/src/chat/chat-view/ChatController.ts +++ b/vscode/src/chat/chat-view/ChatController.ts @@ -294,6 +294,8 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv break } case 'edit': { + await this.cancelSubmitOrEditOperation() + await this.handleEdit({ requestID: uuid.v4(), text: PromptString.unsafe_fromUserQuery(message.text), @@ -1081,16 +1083,12 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv } private submitOrEditOperation: AbortController | undefined - public startNewSubmitOrEditOperation(): Promise { + public startNewSubmitOrEditOperation(): AbortSignal { this.submitOrEditOperation?.abort() - - return new Promise(resolve => { - setTimeout(() => { - this.submitOrEditOperation = new AbortController() - resolve(this.submitOrEditOperation.signal) - }, 500) - }) + this.submitOrEditOperation = new AbortController() + return this.submitOrEditOperation.signal } + private cancelSubmitOrEditOperation(): Promise { if (this.submitOrEditOperation) { this.submitOrEditOperation.abort() @@ -1222,7 +1220,7 @@ export class ChatController implements vscode.Disposable, vscode.WebviewViewProv preDetectedIntentScores?: { intent: string; score: number }[] | undefined | null manuallySelectedIntent?: ChatMessage['intent'] | undefined | null }): Promise { - const abortSignal = await this.startNewSubmitOrEditOperation() + const abortSignal = this.startNewSubmitOrEditOperation() telemetryRecorder.recordEvent('cody.editChatButton', 'clicked', { billingMetadata: { diff --git a/vscode/test/e2e/chat-context.test.ts b/vscode/test/e2e/chat-context.test.ts index dbbdbb878f22..03b7f9241091 100644 --- a/vscode/test/e2e/chat-context.test.ts +++ b/vscode/test/e2e/chat-context.test.ts @@ -8,7 +8,7 @@ import { } from './common' import { test } from './helpers' -test.skip('chat followup context', async ({ page, sidebar }) => { +test('chat followup context', async ({ page, sidebar }) => { await sidebarSignin(page, sidebar) // Open chat. diff --git a/vscode/test/e2e/chat-input.test.ts b/vscode/test/e2e/chat-input.test.ts index edef13300db2..038fd7548fcb 100644 --- a/vscode/test/e2e/chat-input.test.ts +++ b/vscode/test/e2e/chat-input.test.ts @@ -22,77 +22,76 @@ test.extend({ 'cody.chat-question:executed', 'cody.chatResponse:hasCode', ], -}) - .skip('chat input focus', async ({ page, sidebar }) => { - // This test requires that the window be focused in the OS window manager because it deals with - // focus. - await page.bringToFront() +})('chat input focus', async ({ page, sidebar }) => { + // This test requires that the window be focused in the OS window manager because it deals with + // focus. + await page.bringToFront() - await sidebarSignin(page, sidebar) - // Open the buzz.ts file from the tree view, - // and then submit a chat question from the command menu. - await sidebarExplorer(page).click() - await page.getByRole('treeitem', { name: 'buzz.ts' }).locator('a').dblclick() - await page.getByRole('tab', { name: 'buzz.ts' }).hover() - - // Open a new chat panel before opening the file to make sure - // the chat panel is right next to the document. This helps to save loading time - // when we submit a question later as the question will be streamed to this panel - // directly instead of opening a new one. - await page.getByRole('tab', { name: 'Cody', exact: true }).locator('a').click() - const [chatPanel, lastChatInput, firstChatInput, chatInputs] = await createEmptyChatPanel(page) - await expect(firstChatInput).toBeFocused() // Chat should be focused initially. - await page.getByRole('tab', { name: 'Cody', exact: true }).locator('a').click() - await page.getByRole('tab', { name: 'buzz.ts' }).dblclick() - - // Ensure equal-width columns so we can be sure the code we're about to click is in view (and is - // not out of the editor's scroll viewport). This became required due to new (undocumented) - // behavior in VS Code 1.88.0 where the Cody panel would take up ~80% of the width when it was - // focused, meaning that the buzz.ts editor tab would take up ~20% and the text we intend to - // click would be only partially visible, making the click() call fail. - await executeCommandInPalette(page, 'View: Reset Editor Group Sizes') - - // Click in the file to make sure we're not focused in the chat panel. Use the Alt+L hotkey - // (`Cody: New Chat`) to switch back to the chat window we already opened and check that the - // input is focused. - await page.getByText("fizzbuzz.push('Buzz')").click() - - // Submit a new chat question from the command menu. - await page - .locator('[id="workbench\\.parts\\.editor"]') - .getByLabel(/Commands \(/) - .click() - await page.waitForTimeout(100) - - // HACK: The 'delay' command is used to make sure the response is streamed 400ms after - // the command is sent. This provides us with a small window to move the cursor - // from the new opened chat window back to the editor, before the chat has finished - // streaming its response. - await firstChatInput.fill('delay') - await firstChatInput.press('Enter') - await expect(lastChatInput).toBeFocused() - - // Make sure the chat input box does not steal focus from the editor when editor - // is focused. - await expect(lastChatInput).toBeFocused() - await page.getByText("fizzbuzz.push('Buzz')").click() - await expect(firstChatInput).not.toBeFocused() - await expect(lastChatInput).not.toBeFocused() - // once the response is 'Done', check the input focus - await firstChatInput.hover() - await expect(chatPanel.getByText('Done')).toBeVisible() - await expect(firstChatInput).not.toBeFocused() - await expect(lastChatInput).not.toBeFocused() - - // Click into the last chat input and submit a new follow-up chat message. The original focus - // area which is the chat input should still have the focus after the response is received. - await lastChatInput.click() - await lastChatInput.type('Regular chat message', { delay: 10 }) - await lastChatInput.press('Enter') - await expect(chatPanel.getByText('hello from the assistant')).toBeVisible() - await expect(chatInputs.nth(1)).not.toBeFocused() - await expect(lastChatInput).toBeFocused() - }) + await sidebarSignin(page, sidebar) + // Open the buzz.ts file from the tree view, + // and then submit a chat question from the command menu. + await sidebarExplorer(page).click() + await page.getByRole('treeitem', { name: 'buzz.ts' }).locator('a').dblclick() + await page.getByRole('tab', { name: 'buzz.ts' }).hover() + + // Open a new chat panel before opening the file to make sure + // the chat panel is right next to the document. This helps to save loading time + // when we submit a question later as the question will be streamed to this panel + // directly instead of opening a new one. + await page.getByRole('tab', { name: 'Cody', exact: true }).locator('a').click() + const [chatPanel, lastChatInput, firstChatInput, chatInputs] = await createEmptyChatPanel(page) + await expect(firstChatInput).toBeFocused() // Chat should be focused initially. + await page.getByRole('tab', { name: 'Cody', exact: true }).locator('a').click() + await page.getByRole('tab', { name: 'buzz.ts' }).dblclick() + + // Ensure equal-width columns so we can be sure the code we're about to click is in view (and is + // not out of the editor's scroll viewport). This became required due to new (undocumented) + // behavior in VS Code 1.88.0 where the Cody panel would take up ~80% of the width when it was + // focused, meaning that the buzz.ts editor tab would take up ~20% and the text we intend to + // click would be only partially visible, making the click() call fail. + await executeCommandInPalette(page, 'View: Reset Editor Group Sizes') + + // Click in the file to make sure we're not focused in the chat panel. Use the Alt+L hotkey + // (`Cody: New Chat`) to switch back to the chat window we already opened and check that the + // input is focused. + await page.getByText("fizzbuzz.push('Buzz')").click() + + // Submit a new chat question from the command menu. + await page + .locator('[id="workbench\\.parts\\.editor"]') + .getByLabel(/Commands \(/) + .click() + await page.waitForTimeout(100) + + // HACK: The 'delay' command is used to make sure the response is streamed 400ms after + // the command is sent. This provides us with a small window to move the cursor + // from the new opened chat window back to the editor, before the chat has finished + // streaming its response. + await firstChatInput.fill('delay') + await firstChatInput.press('Enter') + await expect(lastChatInput).toBeFocused() + + // Make sure the chat input box does not steal focus from the editor when editor + // is focused. + await expect(lastChatInput).toBeFocused() + await page.getByText("fizzbuzz.push('Buzz')").click() + await expect(firstChatInput).not.toBeFocused() + await expect(lastChatInput).not.toBeFocused() + // once the response is 'Done', check the input focus + await firstChatInput.hover() + await expect(chatPanel.getByText('Done')).toBeVisible() + await expect(firstChatInput).not.toBeFocused() + await expect(lastChatInput).not.toBeFocused() + + // Click into the last chat input and submit a new follow-up chat message. The original focus + // area which is the chat input should still have the focus after the response is received. + await lastChatInput.click() + await lastChatInput.type('Regular chat message', { delay: 10 }) + await lastChatInput.press('Enter') + await expect(chatPanel.getByText('hello from the assistant')).toBeVisible() + await expect(chatInputs.nth(1)).not.toBeFocused() + await expect(lastChatInput).toBeFocused() +}) test.extend({ dotcomUrl: mockServer.SERVER_URL }) .skip('chat toolbar and row UI', async ({ page, sidebar }) => { From bc8f1fb6978a203fa87c12753e59d4061d92095d Mon Sep 17 00:00:00 2001 From: Quinn Slack Date: Thu, 6 Feb 2025 19:01:03 -0800 Subject: [PATCH 06/10] fix ~5px pixel jitter when pressing Enter in chat in VS Code (#6991) Previously, pressing Enter in a chat message in VS Code's Cody chat would cause the iframe to be shifted up by ~5px. This was an annoying visual jitter. Now, this no longer happens. ## Test plan CI Also: Run in VS Code, type a chat message, press Enter, and ensure there is no jitter. Also ask a long chat question and ensure that the scroll-to-end works. ## Changelog - Fixed an issue where pressing Enter in chat would cause brief visual jitter in the UI. --- .../.sourcegraph/no-scrollIntoView.rule.md | 14 ++++++++++++++ vscode/webviews/App.module.css | 2 -- vscode/webviews/CodyPanel.tsx | 2 +- vscode/webviews/chat/Transcript.tsx | 15 +++++++++++++-- 4 files changed, 28 insertions(+), 5 deletions(-) create mode 100644 vscode/webviews/.sourcegraph/no-scrollIntoView.rule.md diff --git a/vscode/webviews/.sourcegraph/no-scrollIntoView.rule.md b/vscode/webviews/.sourcegraph/no-scrollIntoView.rule.md new file mode 100644 index 000000000000..6819e664cfa1 --- /dev/null +++ b/vscode/webviews/.sourcegraph/no-scrollIntoView.rule.md @@ -0,0 +1,14 @@ +--- +title: Do not use scrollIntoView in a VS Code webview +--- + +Using `HTMLElement.scrollIntoView()` in a VS Code webview causes the iframe to be incorrectly positioned, chopping off the top ~5px and adding an empty void in the bottom ~5px. + +Instead, only scroll the nearest ancestor scrollable container: + +```typescript +const container = e.closest('[data-scrollable]') +if (container && container instanceof HTMLElement) { + container.scrollTop = e.offsetTop - container.offsetTop +} +``` \ No newline at end of file diff --git a/vscode/webviews/App.module.css b/vscode/webviews/App.module.css index 3b86a6ca7e76..720e20595ded 100644 --- a/vscode/webviews/App.module.css +++ b/vscode/webviews/App.module.css @@ -1,8 +1,6 @@ .outer-container { - background-color: var(--vscode-sideBar-background); display: flex; flex-direction: column; - box-sizing: border-box; height: 100%; overflow: hidden; } diff --git a/vscode/webviews/CodyPanel.tsx b/vscode/webviews/CodyPanel.tsx index 24e00e37de75..181fbfa0d062 100644 --- a/vscode/webviews/CodyPanel.tsx +++ b/vscode/webviews/CodyPanel.tsx @@ -129,7 +129,7 @@ export const CodyPanel: FunctionComponent = ({ /> )} {errorMessages && } - + {view === View.Chat && ( = memo(props => { export function focusLastHumanMessageEditor(): void { const elements = document.querySelectorAll('[data-lexical-editor]') const lastEditor = elements.item(elements.length - 1) - lastEditor?.focus() - lastEditor?.scrollIntoView() + if (!lastEditor) { + return + } + + lastEditor.focus() + + // Only scroll the nearest scrollable ancestor container, not all scrollable ancestors, to avoid + // a bug in VS Code where the iframe is pushed up by ~5px. + const container = lastEditor?.closest('[data-scrollable]') + const editorScrollItemInContainer = lastEditor.parentElement + if (container && container instanceof HTMLElement && editorScrollItemInContainer) { + container.scrollTop = editorScrollItemInContainer.offsetTop - container.offsetTop + } } export function editHumanMessage({ From 3afac5d28992f61698be3c3753ff630368b01eac Mon Sep 17 00:00:00 2001 From: Dominic Cooney Date: Fri, 7 Feb 2025 15:15:48 +0900 Subject: [PATCH 07/10] chore/release: VSCode stable release Slack announcements to point to the correct release branches (#6975) ## Test plan No test... we will watch what happens in Slack when we release M68 to stable. This is the last step in the job so if it fails the release is still done. The diff URLs look like this now: https://github.com/sourcegraph/cody/compare/M66...M68 --- .github/workflows/release-vscode-stable.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-vscode-stable.yml b/.github/workflows/release-vscode-stable.yml index 73bc13f22116..627db160e49f 100644 --- a/.github/workflows/release-vscode-stable.yml +++ b/.github/workflows/release-vscode-stable.yml @@ -80,8 +80,8 @@ jobs: minor=$(echo $tag | sed 's/\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\)/\2/') next_minor=$(($minor + 2)) echo "VERSION_ANCHOR=$version_anchor" >> $GITHUB_ENV - echo "CURRENT_RELEASE_BRANCH=vscode-v$major.$minor.x" >> $GITHUB_ENV - echo "NEXT_RELEASE_BRANCH=vscode-v$major.$next_minor.x" >> $GITHUB_ENV + echo "CURRENT_RELEASE_BRANCH=M$minor" >> $GITHUB_ENV + echo "NEXT_RELEASE_BRANCH=M$next_minor" >> $GITHUB_ENV - name: "Slack notification" run: | echo "Posting release announcement to Slack" From 0a6db51a2fcc9609f86bb129ede8effe9ca4a7e3 Mon Sep 17 00:00:00 2001 From: Naman Kumar Date: Fri, 7 Feb 2025 12:37:56 +0530 Subject: [PATCH 08/10] Fix Repo filter dropdown z-index (#6994) closes: https://linear.app/sourcegraph/issue/SRCH-1585/followup-box-leaks-through-repo-filters Fix the z-index for repo search filter dropdown. It overlaps over follow up input and breaks. ## Test plan Before image After image --- .../chat/cells/messageCell/assistant/RepositorySelector.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vscode/webviews/chat/cells/messageCell/assistant/RepositorySelector.tsx b/vscode/webviews/chat/cells/messageCell/assistant/RepositorySelector.tsx index 078e89d09566..c0f309fb8b7e 100644 --- a/vscode/webviews/chat/cells/messageCell/assistant/RepositorySelector.tsx +++ b/vscode/webviews/chat/cells/messageCell/assistant/RepositorySelector.tsx @@ -37,7 +37,7 @@ export const RepositorySelector = ({ onSelect }: IProps) => { {open && (
e.preventDefault()} - className="tw-w-[100%] tw-mt-4 tw-absolute tw-top-10 tw-left-0 tw-bg-background tw-text-foreground tw-border tw-border-border tw-rounded-md tw-shadow-md tw-overflow-y-auto tw-max-h-[300px]" + className="tw-w-[100%] tw-mt-4 tw-absolute tw-top-10 tw-left-0 tw-bg-background tw-text-foreground tw-border tw-border-border tw-rounded-md tw-shadow-md tw-overflow-y-auto tw-max-h-[300px] tw-z-10" > {repos.value?.map(repo => ( From 43477d9ebd9225f1d85164750a60c62c904795d5 Mon Sep 17 00:00:00 2001 From: Rob Rhyne Date: Fri, 7 Feb 2025 02:43:01 -0500 Subject: [PATCH 09/10] Fix border on history search (#6983) Removes CSS that was conflicting with default input styles ## Test plan * Manually check to ensure the input has a border. --- vscode/webviews/tabs/HistoryTab.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vscode/webviews/tabs/HistoryTab.tsx b/vscode/webviews/tabs/HistoryTab.tsx index c9c13dd65614..c2ac999ff9c4 100644 --- a/vscode/webviews/tabs/HistoryTab.tsx +++ b/vscode/webviews/tabs/HistoryTab.tsx @@ -104,7 +104,7 @@ export const HistoryTabWithData: React.FC<
setSearchText(event.target.value)} From 25a431a2fa0b413602a8944ea7fcd264b2a18a0d Mon Sep 17 00:00:00 2001 From: Quinn Slack Date: Fri, 7 Feb 2025 09:05:07 -0800 Subject: [PATCH 10/10] simplify chat UI (#6992) - remove switch intent because you can just go back and edit your message and click a different submit btn - do not show noisy blue bg on last-hovered prompt in chat initial view - do not show "Open in Editor" in editor chats - make context cell non-expandable and remove "Public knowledge": It's 2025, everyone knows this already. Removes noise from our UI. - remove "Edit context" buttons: These buttons are rarely used and no longer meet the signal-to-noise ratio now that agentic context is better and that is the user expectation. Users can always manually mention files if needed. - remove speaker names and icons from transcript: Remove speaker icons and names from chat transcript: no human username, no model icon, no model name, no Sourcegraph logo for context. This reduces UI noise significantly. These icons and names are no longer needed because our users are sufficiently familiar with AI chat now. - remove QuickStart and WelcomeFooter: These are probably helpful to some, but they add up to just making our empty state noisy. Great prompts that someone else at your company wrote are a better and more tailored way to get started quickly. - show only 1 guardrails icon --- vscode/test/e2e/chat-input.test.ts | 9 - vscode/webviews/Chat.tsx | 2 - .../GuardRailStatusController.ts | 30 +- vscode/webviews/chat/Transcript.test.tsx | 6 +- vscode/webviews/chat/Transcript.tsx | 101 +----- .../cells/contextCell/ContextCell.module.css | 3 - .../cells/contextCell/ContextCell.story.tsx | 4 +- .../chat/cells/contextCell/ContextCell.tsx | 115 +------ .../cells/messageCell/BaseMessageCell.tsx | 13 +- .../assistant/AssistantMessageCell.tsx | 23 +- .../messageCell/assistant/SwitchIntent.tsx | 45 --- .../messageCell/human/HumanMessageCell.tsx | 6 +- .../webviews/chat/components/QuickStart.tsx | 288 ------------------ .../chat/components/WelcomeFooter.module.css | 80 ----- .../chat/components/WelcomeFooter.tsx | 13 - .../ExtensionPromotionalBanner.module.css | 38 --- .../components/ExtensionPromotionalBanner.tsx | 76 ----- .../components/FileContextItem.module.css | 1 - .../components/promptList/PromptList.tsx | 1 + 19 files changed, 35 insertions(+), 819 deletions(-) delete mode 100644 vscode/webviews/chat/cells/messageCell/assistant/SwitchIntent.tsx delete mode 100644 vscode/webviews/chat/components/QuickStart.tsx delete mode 100644 vscode/webviews/chat/components/WelcomeFooter.module.css delete mode 100644 vscode/webviews/chat/components/WelcomeFooter.tsx delete mode 100644 vscode/webviews/components/ExtensionPromotionalBanner.module.css delete mode 100644 vscode/webviews/components/ExtensionPromotionalBanner.tsx diff --git a/vscode/test/e2e/chat-input.test.ts b/vscode/test/e2e/chat-input.test.ts index 038fd7548fcb..9b524e159e73 100644 --- a/vscode/test/e2e/chat-input.test.ts +++ b/vscode/test/e2e/chat-input.test.ts @@ -203,13 +203,6 @@ test.extend({ dotcomUrl: mockServer.SERVER_URL }).extend { - await expect(chatFrame.locator('[data-testid="chat-model"]').last()).toHaveText(modelName) - } - - // Verify tooltip shows the correct model - await expectModelName('Claude 3.5 Sonnet') - // Change model and send another message. await expect(modelSelect).toBeEnabled() await modelSelect.click() @@ -220,8 +213,6 @@ test.extend({ dotcomUrl: mockServer.SERVER_URL }).extend({ diff --git a/vscode/webviews/Chat.tsx b/vscode/webviews/Chat.tsx index 78465faf94d2..c846f34b8b39 100644 --- a/vscode/webviews/Chat.tsx +++ b/vscode/webviews/Chat.tsx @@ -14,7 +14,6 @@ import type { VSCodeWrapper } from './utils/VSCodeApi' import type { Context } from '@opentelemetry/api' import styles from './Chat.module.css' -import WelcomeFooter from './chat/components/WelcomeFooter' import { WelcomeMessage } from './chat/components/WelcomeMessage' import { WelcomeNotice } from './chat/components/WelcomeNotice' import { ScrollDown } from './components/ScrollDown' @@ -224,7 +223,6 @@ export const Chat: React.FunctionComponent setView={setView} isPromptsV2Enabled={isPromptsV2Enabled} /> - {isWorkspacesUpgradeCtaEnabled && userInfo.IDE !== CodyIDE.Web && (
diff --git a/vscode/webviews/chat/ChatMessageContent/GuardRailStatusController.ts b/vscode/webviews/chat/ChatMessageContent/GuardRailStatusController.ts index e4feb335fb85..676ef752c258 100644 --- a/vscode/webviews/chat/ChatMessageContent/GuardRailStatusController.ts +++ b/vscode/webviews/chat/ChatMessageContent/GuardRailStatusController.ts @@ -1,5 +1,3 @@ -import { ShieldIcon } from '../../icons/CodeBlockActionIcons' - import styles from './ChatMessageContent.module.css' /* @@ -19,18 +17,16 @@ export class GuardrailsStatusController { private status: HTMLElement constructor(public container: HTMLElement) { - this.findOrAppend(this.iconClass, () => { - const icon = document.createElement('div') - icon.innerHTML = ShieldIcon - icon.classList.add(styles.attributionIcon, this.iconClass) - icon.setAttribute('data-testid', 'attribution-indicator') - return icon - }) - this.status = this.findOrAppend(this.statusClass, () => { + const elements = this.container.getElementsByClassName(this.statusClass) + if (elements.length > 0) { + this.status = elements[0] as HTMLElement + } else { const status = document.createElement('div') status.classList.add(styles.status, this.statusClass) - return status - }) + status.setAttribute('data-testid', 'attribution-indicator') + this.container.append(status) + this.status = status + } } /** @@ -73,16 +69,6 @@ export class GuardrailsStatusController { this.status.innerHTML = this.statusUnavailable } - private findOrAppend(className: string, make: () => HTMLElement): HTMLElement { - const elements = this.container.getElementsByClassName(className) - if (elements.length > 0) { - return elements[0] as HTMLElement - } - const newElement = make() - this.container.append(newElement) - return newElement - } - private tooltip(repos: string[], limitHit: boolean) { const prefix = 'Guardrails check failed. Code found in' if (repos.length === 1) { diff --git a/vscode/webviews/chat/Transcript.test.tsx b/vscode/webviews/chat/Transcript.test.tsx index 5dc4b3728af5..2d2ca665a015 100644 --- a/vscode/webviews/chat/Transcript.test.tsx +++ b/vscode/webviews/chat/Transcript.test.tsx @@ -247,11 +247,7 @@ describe('Transcript', () => { ]} /> ) - expectCells([ - { message: 'Foo' }, - { context: {} }, - { message: 'Model\n\nRequest Failed: some error' }, - ]) + expectCells([{ message: 'Foo' }, { context: {} }, { message: 'Request Failed: some error' }]) }) test('does not clobber user input into followup while isPendingPriorResponse when it completes', async () => { diff --git a/vscode/webviews/chat/Transcript.tsx b/vscode/webviews/chat/Transcript.tsx index 7a665fcd2b33..9a38189088c6 100644 --- a/vscode/webviews/chat/Transcript.tsx +++ b/vscode/webviews/chat/Transcript.tsx @@ -11,11 +11,7 @@ import { isAbortErrorOrSocketHangUp, serializedPromptEditorStateFromText, } from '@sourcegraph/cody-shared' -import { - type PromptEditorRefAPI, - useDefaultContextForChat, - useExtensionAPI, -} from '@sourcegraph/prompt-editor' +import { type PromptEditorRefAPI, useExtensionAPI } from '@sourcegraph/prompt-editor' import { clsx } from 'clsx' import { isEqual } from 'lodash' import debounce from 'lodash/debounce' @@ -35,14 +31,9 @@ import type { UserAccountInfo } from '../Chat' import type { ApiPostMessage } from '../Chat' import { getVSCodeAPI } from '../utils/VSCodeApi' import { SpanManager } from '../utils/spanManager' -import { getTraceparentFromSpanContext, useTelemetryRecorder } from '../utils/telemetry' +import { getTraceparentFromSpanContext } from '../utils/telemetry' import { useOmniBox } from '../utils/useOmniBox' import type { CodeBlockActionsProps } from './ChatMessageContent/ChatMessageContent' -import { - ContextCell, - EditContextButtonChat, - EditContextButtonSearch, -} from './cells/contextCell/ContextCell' import { AssistantMessageCell, makeHumanMessageInfo, @@ -52,12 +43,11 @@ import { HumanMessageCell } from './cells/messageCell/human/HumanMessageCell' import { type Context, type Span, context, trace } from '@opentelemetry/api' import { DeepCodyAgentID, ToolCodyModelName } from '@sourcegraph/cody-shared/src/models/client' import { isCodeSearchContextItem } from '../../src/context/openctx/codeSearch' -import { TELEMETRY_INTENT } from '../../src/telemetry/onebox' import { useIntentDetectionConfig } from '../components/omnibox/intentDetection' import { AgenticContextCell } from './cells/agenticCell/AgenticContextCell' import ApprovalCell from './cells/agenticCell/ApprovalCell' +import { ContextCell } from './cells/contextCell/ContextCell' import { DidYouMeanNotice } from './cells/messageCell/assistant/DidYouMean' -import { SwitchIntent } from './cells/messageCell/assistant/SwitchIntent' import { LastEditorContext } from './context' interface TranscriptProps { @@ -136,7 +126,7 @@ export const Transcript: FC = props => { return (
0, })} > @@ -595,64 +585,6 @@ const TranscriptInteraction: FC = memo(props => { return null }, [humanMessage, assistantMessage, isContextLoading]) - const telemetryRecorder = useTelemetryRecorder() - const reSubmitWithIntent = useCallback( - (intent: ChatMessage['intent']) => { - const editorState = humanEditorRef.current?.getSerializedValue() - if (editorState) { - onEditSubmit(intent) - telemetryRecorder.recordEvent('onebox.intentCorrection', 'clicked', { - metadata: { - initialIntent: - humanMessage.intent === 'search' - ? TELEMETRY_INTENT.SEARCH - : TELEMETRY_INTENT.CHAT, - selectedIntent: - intent === 'search' ? TELEMETRY_INTENT.SEARCH : TELEMETRY_INTENT.CHAT, - }, - privateMetadata: { - query: editorState.text, - }, - billingMetadata: { product: 'cody', category: 'billable' }, - }) - } - }, - [onEditSubmit, telemetryRecorder, humanMessage] - ) - - const { corpusContext: corpusContextItems } = useDefaultContextForChat() - const resubmitWithRepoContext = useCallback(async () => { - const editorState = humanEditorRef.current?.getSerializedValue() - if (editorState) { - const editor = humanEditorRef.current - if (corpusContextItems.length === 0 || !editor) { - return - } - await editor.addMentions(corpusContextItems, 'before', ' ') - onEditSubmit('chat') - } - }, [corpusContextItems, onEditSubmit]) - - const reSubmitWithChatIntent = useCallback(() => reSubmitWithIntent('chat'), [reSubmitWithIntent]) - const reSubmitWithSearchIntent = useCallback( - () => reSubmitWithIntent('search'), - [reSubmitWithIntent] - ) - - const manuallyEditContext = useCallback(() => { - const contextFiles = humanMessage.contextFiles - const editor = humanEditorRef.current - if (!contextFiles || !editor) { - return - } - editor.filterMentions(item => item.type !== 'repository') - editor.addMentions(contextFiles, 'before', '\n') - }, [humanMessage.contextFiles]) - - const mentionsContainRepository = humanEditorRef.current - ?.getSerializedValue() - .contextItems.some(item => item.type === 'repository') - const onHumanMessageSubmit = useCallback( (intent?: ChatMessage['intent']) => { if (humanMessage.isUnsentFollowup) { @@ -710,17 +642,6 @@ const TranscriptInteraction: FC = memo(props => { intent={manuallySelectedIntent || intentResults?.intent} manuallySelectIntent={setManuallySelectedIntent} /> - {experimentalOneBoxEnabled && ( - - )} {experimentalOneBoxEnabled && assistantMessage?.didYouMeanQuery && ( = memo(props => { 0 && - !mentionsContainRepository && - assistantMessage - ? resubmitWithRepoContext - : undefined - } key={`${humanMessage.index}-${humanMessage.intent}-context`} contextItems={humanMessage.contextFiles} contextAlternatives={humanMessage.contextAlternatives} model={assistantMessage?.model} isForFirstMessage={humanMessage.index === 0} isContextLoading={isContextLoading} - onManuallyEditContext={manuallyEditContext} - editContextNode={ - humanMessage.intent === 'search' - ? EditContextButtonSearch - : EditContextButtonChat - } defaultOpen={isContextLoading && humanMessage.agent === DeepCodyAgentID} - processes={humanMessage?.processes ?? undefined} agent={humanMessage?.agent ?? undefined} /> )} diff --git a/vscode/webviews/chat/cells/contextCell/ContextCell.module.css b/vscode/webviews/chat/cells/contextCell/ContextCell.module.css index 47b69062bef1..b893f78afe91 100644 --- a/vscode/webviews/chat/cells/contextCell/ContextCell.module.css +++ b/vscode/webviews/chat/cells/contextCell/ContextCell.module.css @@ -1,6 +1,5 @@ .context-item { display: inline-flex; - padding: 2px 4px 2px 2px; } .context-item-metadata { @@ -13,7 +12,6 @@ /* display: flex; */ display: none; align-items: center; - flex-wrap: nowrap; } @@ -36,7 +34,6 @@ .link-container { display: inline-flex; - padding: 2px 4px 2px 2px; min-width: 0; } diff --git a/vscode/webviews/chat/cells/contextCell/ContextCell.story.tsx b/vscode/webviews/chat/cells/contextCell/ContextCell.story.tsx index f6e87814f923..884cd74d0f66 100644 --- a/vscode/webviews/chat/cells/contextCell/ContextCell.story.tsx +++ b/vscode/webviews/chat/cells/contextCell/ContextCell.story.tsx @@ -4,7 +4,7 @@ import { ContextItemSource } from '@sourcegraph/cody-shared' import type { ComponentProps } from 'react' import { URI } from 'vscode-uri' import { VSCodeStandaloneComponent } from '../../../storybook/VSCodeStoryDecorator' -import { ContextCell, EditContextButtonChat, __ContextCellStorybookContext } from './ContextCell' +import { ContextCell, __ContextCellStorybookContext } from './ContextCell' const renderWithInitialOpen = (args: ComponentProps) => { return ( @@ -20,7 +20,6 @@ const meta: Meta = { decorators: [VSCodeStandaloneComponent], args: { isForFirstMessage: true, - editContextNode: EditContextButtonChat, }, render: renderWithInitialOpen, } @@ -158,7 +157,6 @@ export const ExcludedContext: Story = { export const NoContextRequested: Story = { args: { contextItems: undefined, - resubmitWithRepoContext: () => Promise.resolve(), isForFirstMessage: true, }, } diff --git a/vscode/webviews/chat/cells/contextCell/ContextCell.tsx b/vscode/webviews/chat/cells/contextCell/ContextCell.tsx index 707107262a9c..0e760606f03a 100644 --- a/vscode/webviews/chat/cells/contextCell/ContextCell.tsx +++ b/vscode/webviews/chat/cells/contextCell/ContextCell.tsx @@ -1,15 +1,9 @@ -import type { - ChatMessage, - ContextItem, - Model, - ProcessingStep, - RankedContext, -} from '@sourcegraph/cody-shared' +import type { ChatMessage, ContextItem, Model, RankedContext } from '@sourcegraph/cody-shared' import { pluralize } from '@sourcegraph/cody-shared' import { DeepCodyAgentID } from '@sourcegraph/cody-shared/src/models/client' import { MENTION_CLASS_NAME } from '@sourcegraph/prompt-editor' import { clsx } from 'clsx' -import { BrainIcon, FilePenLine, MessagesSquareIcon } from 'lucide-react' +import { BrainIcon, MessagesSquareIcon } from 'lucide-react' import { type FunctionComponent, createContext, memo, useCallback, useContext, useState } from 'react' import { FileLink } from '../../../components/FileLink' import { @@ -18,14 +12,11 @@ import { AccordionItem, AccordionTrigger, } from '../../../components/shadcn/ui/accordion' -import { Button } from '../../../components/shadcn/ui/button' import { Tooltip, TooltipContent, TooltipTrigger } from '../../../components/shadcn/ui/tooltip' -import { SourcegraphLogo } from '../../../icons/SourcegraphLogo' import { useTelemetryRecorder } from '../../../utils/telemetry' import { useConfig } from '../../../utils/useConfig' import { LoadingDots } from '../../components/LoadingDots' import { Cell } from '../Cell' -import { NON_HUMAN_CELL_AVATAR_SIZE } from '../messageCell/assistant/AssistantMessageCell' import styles from './ContextCell.module.css' export const __ContextCellStorybookContext = createContext<{ @@ -39,7 +30,6 @@ export const ContextCell: FunctionComponent<{ isContextLoading: boolean contextItems: ContextItem[] | undefined contextAlternatives?: RankedContext[] - resubmitWithRepoContext?: () => Promise isForFirstMessage: boolean @@ -49,27 +39,20 @@ export const ContextCell: FunctionComponent<{ defaultOpen?: boolean intent: ChatMessage['intent'] - onManuallyEditContext: () => void - editContextNode: React.ReactNode experimentalOneBoxEnabled?: boolean - processes?: ProcessingStep[] agent?: string }> = memo( ({ contextItems, contextAlternatives, - resubmitWithRepoContext, model, isForFirstMessage, className, defaultOpen, isContextLoading, - onManuallyEditContext, - editContextNode, intent, experimentalOneBoxEnabled, - processes, agent, }) => { const __storybook__initialOpen = useContext(__ContextCellStorybookContext)?.initialOpen ?? false @@ -125,11 +108,6 @@ export const ContextCell: FunctionComponent<{ }) }, [excludedContext.length, usedContext]) - const onEditContext = useCallback(() => { - triggerAccordion() - onManuallyEditContext() - }, [triggerAccordion, onManuallyEditContext]) - const { config: { internalDebugContext }, } = useConfig() @@ -164,8 +142,14 @@ export const ContextCell: FunctionComponent<{ : itemCountLabel, } + const hasContent = + isContextLoading || + (contextItemsToDisplay && contextItemsToDisplay.length > 0) || + !isForFirstMessage || + isAgenticChat + return ( -
+
- {headerText.main} {headerText.sub && ( - — {headerText.sub} + — {headerText.sub} )} @@ -205,43 +185,16 @@ export const ContextCell: FunctionComponent<{ ) : ( <> -
- {contextItems && - contextItems.length > 0 && - !isAgenticChat && ( - - )} - {resubmitWithRepoContext && !isAgenticChat && ( - - )} -
{internalDebugContext && contextAlternatives && (
{' '} Ranking mechanism:{' '} {selectedAlternative === undefined @@ -332,31 +285,6 @@ export const ContextCell: FunctionComponent<{ )} - {!isContextLoading && !isAgenticChat && ( -
  • - - - - - Public knowledge - - - - Information and general reasoning - capabilities trained into the model{' '} - {model && {model}} - - -
  • - )} @@ -376,7 +304,6 @@ export const ContextCell: FunctionComponent<{ ) } ) - const getContextInfo = (items?: ContextItem[], isFirst?: boolean) => { const { usedContext, excludedContext, count } = (items ?? []).reduce( (acc, item) => { @@ -430,17 +357,3 @@ const ExcludedContextWarning: React.FC<{ message: string }> = ({ message }) => (
    ) - -export const EditContextButtonSearch = ( - <> - -
    Edit results
    - -) - -export const EditContextButtonChat = ( - <> - -
    Edit context
    - -) diff --git a/vscode/webviews/chat/cells/messageCell/BaseMessageCell.tsx b/vscode/webviews/chat/cells/messageCell/BaseMessageCell.tsx index 7b1562d9524d..ea9157e2ae5e 100644 --- a/vscode/webviews/chat/cells/messageCell/BaseMessageCell.tsx +++ b/vscode/webviews/chat/cells/messageCell/BaseMessageCell.tsx @@ -5,21 +5,14 @@ import { Cell } from '../Cell' * The base component for messages. */ export const BaseMessageCell: FunctionComponent<{ - speakerIcon?: React.ReactNode - speakerTitle?: React.ReactNode cellAction?: React.ReactNode content: React.ReactNode contentClassName?: string footer?: React.ReactNode className?: string -}> = ({ speakerIcon, speakerTitle, cellAction, content, contentClassName, footer, className }) => ( +}> = ({ cellAction, content, contentClassName, footer, className }) => ( - {speakerIcon} {speakerTitle} -
    {cellAction}
    - - } + header={
    {cellAction}
    } containerClassName={className} contentClassName={contentClassName} data-testid="message" @@ -28,5 +21,3 @@ export const BaseMessageCell: FunctionComponent<{ {footer}
    ) - -export const MESSAGE_CELL_AVATAR_SIZE = 22 diff --git a/vscode/webviews/chat/cells/messageCell/assistant/AssistantMessageCell.tsx b/vscode/webviews/chat/cells/messageCell/assistant/AssistantMessageCell.tsx index a59ea5c7d724..30c2fdaf8584 100644 --- a/vscode/webviews/chat/cells/messageCell/assistant/AssistantMessageCell.tsx +++ b/vscode/webviews/chat/cells/messageCell/assistant/AssistantMessageCell.tsx @@ -18,7 +18,6 @@ import type { PromptEditorRefAPI } from '@sourcegraph/prompt-editor' import isEqual from 'lodash/isEqual' import { type FunctionComponent, type RefObject, memo, useMemo } from 'react' import type { ApiPostMessage, UserAccountInfo } from '../../../../Chat' -import { chatModelIconComponent } from '../../../../components/ChatModelIcon' import { useOmniBox } from '../../../../utils/useOmniBox' import { ChatMessageContent, @@ -27,7 +26,7 @@ import { import { ErrorItem, RequestErrorItem } from '../../../ErrorItem' import { type Interaction, editHumanMessage } from '../../../Transcript' import { LoadingDots } from '../../../components/LoadingDots' -import { BaseMessageCell, MESSAGE_CELL_AVATAR_SIZE } from '../BaseMessageCell' +import { BaseMessageCell } from '../BaseMessageCell' import { SearchResults } from './SearchResults' import { SubMessageCell } from './SubMessageCell' @@ -77,7 +76,6 @@ export const AssistantMessageCell: FunctionComponent<{ ) const chatModel = useChatModelByID(message.model, models) - const ModelIcon = chatModel ? chatModelIconComponent(chatModel.id) : null const isAborted = isAbortErrorOrSocketHangUp(message.error) const hasLongerResponseTime = chatModel?.tags?.includes(ModelTag.StreamDisabled) @@ -88,22 +86,6 @@ export const AssistantMessageCell: FunctionComponent<{ return ( - ) : null - } - speakerTitle={ - isSearchIntent ? undefined : ( - - {chatModel - ? chatModel.id.includes(DeepCodyAgentID) - ? 'Claude 3.5 Sonnet (New)' - : chatModel.title ?? `Model ${chatModel.id} by ${chatModel.provider}` - : 'Model'} - - ) - } content={ <> {message.error && !isAborted ? ( @@ -176,9 +158,6 @@ export const AssistantMessageCell: FunctionComponent<{ isEqual ) -export const NON_HUMAN_CELL_AVATAR_SIZE = - MESSAGE_CELL_AVATAR_SIZE * 0.83 /* make them "look" the same size as the human avatar icons */ - export interface HumanMessageInitialContextInfo { repositories: boolean files: boolean diff --git a/vscode/webviews/chat/cells/messageCell/assistant/SwitchIntent.tsx b/vscode/webviews/chat/cells/messageCell/assistant/SwitchIntent.tsx deleted file mode 100644 index b3963c555629..000000000000 --- a/vscode/webviews/chat/cells/messageCell/assistant/SwitchIntent.tsx +++ /dev/null @@ -1,45 +0,0 @@ -import type { ChatMessage } from '@sourcegraph/cody-shared' -import { Brain, MessageSquare, Search, UserCircle2 } from 'lucide-react' -import { Button } from '../../../../components/shadcn/ui/button' - -interface SwitchIntentProps { - intent: ChatMessage['intent'] - manuallySelected: boolean - onSwitch?: () => void -} -export const SwitchIntent = ({ intent, manuallySelected, onSwitch }: SwitchIntentProps) => { - if (!['chat', 'search'].includes(intent || '')) { - return null - } - - return ( -
    -
    - {manuallySelected ? ( - - ) : ( - - )} - - {manuallySelected ? 'You' : 'Intent detection'} selected a{' '} - {intent === 'search' ? 'code search' : 'chat'} response - -
    -
    - -
    -
    - ) -} diff --git a/vscode/webviews/chat/cells/messageCell/human/HumanMessageCell.tsx b/vscode/webviews/chat/cells/messageCell/human/HumanMessageCell.tsx index 0059e86adca4..471019d08b05 100644 --- a/vscode/webviews/chat/cells/messageCell/human/HumanMessageCell.tsx +++ b/vscode/webviews/chat/cells/messageCell/human/HumanMessageCell.tsx @@ -93,7 +93,6 @@ const HumanMessageCellContent = memo(props => { return ( {isFirstMessage && } @@ -130,12 +129,13 @@ const HumanMessageCellContent = memo(props => { /> ) }, isEqual) + const OpenInNewEditorAction = () => { const { - config: { multipleWebviewsEnabled }, + config: { multipleWebviewsEnabled, webviewType }, } = useConfig() - if (!multipleWebviewsEnabled) { + if (!multipleWebviewsEnabled || webviewType !== 'sidebar') { return null } diff --git a/vscode/webviews/chat/components/QuickStart.tsx b/vscode/webviews/chat/components/QuickStart.tsx deleted file mode 100644 index e6321373998e..000000000000 --- a/vscode/webviews/chat/components/QuickStart.tsx +++ /dev/null @@ -1,288 +0,0 @@ -import { useState } from 'react' -import styles from './WelcomeFooter.module.css' - -import { - AtSignIcon, - ChevronDown, - ChevronRight, - type LucideIcon, - MessageSquarePlus, - TextSelect, - X, - Zap, -} from 'lucide-react' - -interface ChatViewTip { - message: string - icon: LucideIcon - vsCodeOnly: boolean -} - -const chatTips: ChatViewTip[] = [ - { - message: 'Type @ to add context to your chat', - icon: AtSignIcon, - vsCodeOnly: true, - }, - { - message: 'Start a new chat with ⇧ ⌥ L or switch to chat with ⌥ /', - icon: MessageSquarePlus, - vsCodeOnly: false, - }, - { - message: 'To add code context from an editor, right click and use Add to Cody Chat', - icon: TextSelect, - vsCodeOnly: true, - }, -] - -interface Example { - input: string - description: string - maxWidth?: string -} - -interface ExampleGroup { - title?: string - input?: string - description?: string - examples?: Example[] -} - -export function QuickStart() { - const [showTipsOverlay, setShowTipsOverlay] = useState(false) - const [isCollapsed, setIsCollapsed] = useState(() => { - const saved = localStorage.getItem('quickStartCollapsed') - return saved ? JSON.parse(saved) : false - }) - const examples: Example[] = [ - { - input: '= useCallback(', - description: 'Deterministically find symbols', - }, - { - input: '"// TODO"', - description: 'Find string literals', - }, - { - input: 'How does this file handle error cases?', - description: 'Ask questions in natural language', - }, - ] - - const allExamples: ExampleGroup[] = [ - ...examples, - { - title: 'Combine search and chat for power usage', - examples: [ - { - input: 'HttpError', - description: 'Start with a search query', - }, - { - input: 'Analyze these error handling implementations and explain our retry and timeout strategy', - description: 'Follow-up with a question about the results returned', - maxWidth: '320px', - }, - ], - }, - ] - - const handleKeyDown = (event: React.KeyboardEvent) => { - if (event.key === 'Enter' || event.key === ' ') { - setShowTipsOverlay(true) - } - } - - const handleOverlayKeyDown = (event: React.KeyboardEvent) => { - if (event.key === 'Enter' || event.key === ' ') { - setShowTipsOverlay(false) - } - } - - const handleOverlayClick = (event: React.MouseEvent) => { - if (event.target === event.currentTarget) { - setShowTipsOverlay(false) - } - } - - const toggleCollapse = () => { - setIsCollapsed((prevState: boolean) => { - const newState = !prevState - localStorage.setItem('quickStartCollapsed', JSON.stringify(newState)) - return newState - }) - } - - const handleCollapseKeyDown = (event: React.KeyboardEvent) => { - if (event.key === 'Enter' || event.key === ' ') { - toggleCollapse() - } - } - - return ( - <> -
    -
    -
    - - Quick Start -
    -
    - {isCollapsed ? ( - - ) : ( - - )} -
    -
    -
    -
    - {examples.map(example => ( -
    -
    - {example.input} -
    -
    - {example.description} -
    -
    - ))} -
    -
    - -
    -
    -
    - - {/* Overlay */} - {showTipsOverlay && ( -
    -
    - {/* Overlay content */} -
    -
    - - Quick Start -
    - -
    -
    -
    - {/* Render all examples including nested ones */} - {allExamples.map(example => ( -
    - {'title' in example && ( -

    - {example.title} -

    - )} - {'examples' in example ? ( -
    - {example.examples?.map(ex => ( -
    -
    - {ex.input} -
    -
    - {ex.description} -
    -
    - ))} -
    - ) : ( - <> -
    - {example.input} -
    -
    - {example.description} -
    - - )} -
    - ))} -
    -
    -
    - {chatTips.map(tip => ( -
    - - - {tip.message} - -
    - ))} -
    -
    -
    -
    -
    - )} - - ) -} diff --git a/vscode/webviews/chat/components/WelcomeFooter.module.css b/vscode/webviews/chat/components/WelcomeFooter.module.css deleted file mode 100644 index ab48422a03cd..000000000000 --- a/vscode/webviews/chat/components/WelcomeFooter.module.css +++ /dev/null @@ -1,80 +0,0 @@ -.welcome-footer { - padding: 1rem; - display: flex; - flex-flow: column nowrap; - max-width: 100%; - color: var(--vscode-input-placeholderForeground) -} - -.cheatsheet { - display: flex; - flex-flow: column nowrap; - width: 100%; - gap: 0.5rem; - padding: 1rem 0; -} - -.title { - display: flex; - flex-flow: row nowrap; - align-items: center; - font-weight: 500; - gap: 0.5rem; -} - -.examples { - display: flex; - flex-flow: row wrap; - gap: 0.25rem 1rem; - padding: 0 0.5rem; -} - -.example { - display: flex; - flex-flow: column nowrap; - align-items: flex-start; - gap: 0.25rem; - padding: 0.25rem 0; -} - -.example-input { - display: flex; - flex-flow: row wrap; - border-radius: 4px; - color: var(--vscode-input-foreground); - border: 1px solid var(--vscode-button-secondaryBackground); - background-color: var(--vscode-editor-background); - font-family: var(--vscode-editor-font-family); - font-size: var(--vscode-editor-font-size); -} - -.tips { - display: flex; - flex-flow: column nowrap; - width: 100%; - gap: 0.5rem; - padding: 1rem 0; -} - -.links { - display: flex; - flex-flow: row wrap; - justify-content: center; - padding: 1rem 0; - gap: 1.25rem; - border-top: 0.5px solid var(--vscode-button-secondaryBackground); -} - -.item { - display: flex; - flex-flow: row nowrap; - flex-shrink: 0; - padding: 0 0.5rem; - gap: 0.5rem; - align-items: center; -} - -.link { - color: inherit; - text-decoration: none; -} diff --git a/vscode/webviews/chat/components/WelcomeFooter.tsx b/vscode/webviews/chat/components/WelcomeFooter.tsx deleted file mode 100644 index e37d7439d88d..000000000000 --- a/vscode/webviews/chat/components/WelcomeFooter.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { CodyIDE } from '@sourcegraph/cody-shared' -import { ExtensionPromotionalBanner } from '../../components/ExtensionPromotionalBanner' -import { QuickStart } from './QuickStart' -import styles from './WelcomeFooter.module.css' - -export default function WelcomeFooter({ IDE }: { IDE: CodyIDE }): JSX.Element { - return ( -
    - {IDE === CodyIDE.Web && } - -
    - ) -} diff --git a/vscode/webviews/components/ExtensionPromotionalBanner.module.css b/vscode/webviews/components/ExtensionPromotionalBanner.module.css deleted file mode 100644 index a4a38c90c16a..000000000000 --- a/vscode/webviews/components/ExtensionPromotionalBanner.module.css +++ /dev/null @@ -1,38 +0,0 @@ -.banner { - display: flex; - flex-flow: row nowrap; - justify-content: space-between; - padding: 0.5rem 1rem; - background: var(--vscode-editor-background); - border: 1px solid var(--vscode-widget-border); - margin: 0 auto; - border-radius: 6px; - - h3 { - margin: 0; - font-size: 0.875rem; - color: var(--vscode-editor-foreground); - font-weight: 500; - } -} - -.download-button { - display: flex; - align-items: center; - padding: 0.25rem 0.75rem; - font-weight: 500; - background: var(--vscode-button-secondaryBackground); - color: var(--vscode-editor-foreground); - text-decoration: none; - border-radius: 4px; - height: 32px; -} - -.download-button:hover { - background: var(--vscode-button-hoverBackground); - color: var(--vscode-editor-foreground); -} - -.banner { - position: relative; -} diff --git a/vscode/webviews/components/ExtensionPromotionalBanner.tsx b/vscode/webviews/components/ExtensionPromotionalBanner.tsx deleted file mode 100644 index 20cb331370a9..000000000000 --- a/vscode/webviews/components/ExtensionPromotionalBanner.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import type { CodyIDE } from '@sourcegraph/cody-shared' -import { useState } from 'react' -import styles from './ExtensionPromotionalBanner.module.css' - -const BANNER_DISMISSED_KEY = 'cody-extension-banner-dismissed' - -export const ExtensionPromotionalBanner: React.FC<{ IDE: CodyIDE }> = ({ IDE }) => { - const [isVisible, setIsVisible] = useState(() => { - // Initialize state from localStorage - return localStorage.getItem(BANNER_DISMISSED_KEY) !== 'true' - }) - const [isClosing, setIsClosing] = useState(false) - - const handleDismiss = () => { - setIsClosing(true) - // Wait for animation to complete before hiding - setTimeout(() => { - setIsVisible(false) - // Save dismissed state to localStorage - localStorage.setItem(BANNER_DISMISSED_KEY, 'true') - }, 300) - } - - if (!isVisible) { - return null - } - - return ( -
    -
    -
    -

    Get Sourcegraph for your favorite editor

    -

    - Download the extension to get the power of Sourcegraph right where you code -

    -
    -
    -
    -
    - VS Code - All JetBrains IDEs -
    - - Download - - -
    -
    - ) -} diff --git a/vscode/webviews/components/FileContextItem.module.css b/vscode/webviews/components/FileContextItem.module.css index 09f9cc03e21d..3aa3b5e372a2 100644 --- a/vscode/webviews/components/FileContextItem.module.css +++ b/vscode/webviews/components/FileContextItem.module.css @@ -6,7 +6,6 @@ .link-container { display: inline-flex; - padding: 2px 4px 2px 2px; min-width: 0; } diff --git a/vscode/webviews/components/promptList/PromptList.tsx b/vscode/webviews/components/promptList/PromptList.tsx index 67086ccf8374..0eedb3f971da 100644 --- a/vscode/webviews/components/promptList/PromptList.tsx +++ b/vscode/webviews/components/promptList/PromptList.tsx @@ -200,6 +200,7 @@ export const PromptList: FC = props => { className={clsx(className, styles.list, { [styles.listChips]: appearanceMode === 'chips-list', })} + disablePointerSelection={true} > {showSearch && (