mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 15:12:02 +00:00
Cody: Add Task View Base for Non-Stop Cody (#52282)
Adds skeleton tree view for Non-Stop Cody
This commit is contained in:
parent
ce3bccdf3d
commit
72ee67fe29
1
client/cody-shared/BUILD.bazel
generated
1
client/cody-shared/BUILD.bazel
generated
@ -45,6 +45,7 @@ ts_project(
|
||||
"src/chat/recipes/inline-chat.ts",
|
||||
"src/chat/recipes/langs.ts",
|
||||
"src/chat/recipes/next-questions.ts",
|
||||
"src/chat/recipes/non-stop.ts",
|
||||
"src/chat/recipes/optimize-code.ts",
|
||||
"src/chat/recipes/recipe.ts",
|
||||
"src/chat/recipes/translate.ts",
|
||||
|
||||
@ -13,7 +13,7 @@ export class Fixup implements Recipe {
|
||||
|
||||
public async getInteraction(humanChatInput: string, context: RecipeContext): Promise<Interaction | null> {
|
||||
// TODO: Prompt the user for additional direction.
|
||||
const selection = context.editor.getActiveTextEditorSelection() || context.editor.controller?.selection
|
||||
const selection = context.editor.getActiveTextEditorSelection() || context.editor.controllers?.inline.selection
|
||||
if (!selection) {
|
||||
await context.editor.showWarningMessage('Select some code to fixup.')
|
||||
return null
|
||||
|
||||
@ -13,7 +13,7 @@ export class InlineChat implements Recipe {
|
||||
public id: RecipeID = 'inline-chat'
|
||||
|
||||
public async getInteraction(humanChatInput: string, context: RecipeContext): Promise<Interaction | null> {
|
||||
const selection = context.editor.controller?.selection
|
||||
const selection = context.editor.controllers?.inline.selection
|
||||
if (!humanChatInput || !selection) {
|
||||
await context.editor.showWarningMessage('Failed to start Inline Chat: empty input or selection.')
|
||||
return null
|
||||
|
||||
112
client/cody-shared/src/chat/recipes/non-stop.ts
Normal file
112
client/cody-shared/src/chat/recipes/non-stop.ts
Normal file
@ -0,0 +1,112 @@
|
||||
import { CodebaseContext } from '../../codebase-context'
|
||||
import { ContextMessage } from '../../codebase-context/messages'
|
||||
import { MAX_CURRENT_FILE_TOKENS, MAX_HUMAN_INPUT_TOKENS } from '../../prompt/constants'
|
||||
import { truncateText, truncateTextStart } from '../../prompt/truncation'
|
||||
import { BufferedBotResponseSubscriber } from '../bot-response-multiplexer'
|
||||
import { Interaction } from '../transcript/interaction'
|
||||
|
||||
import { Recipe, RecipeContext, RecipeID } from './recipe'
|
||||
|
||||
// TODO: Disconnect recipe from chat
|
||||
export class NonStop implements Recipe {
|
||||
public id: RecipeID = 'non-stop'
|
||||
|
||||
public async getInteraction(humanChatInput: string, context: RecipeContext): Promise<Interaction | null> {
|
||||
const controllers = context.editor.controllers
|
||||
const selection = context.editor.getActiveTextEditorSelection()
|
||||
|
||||
if (!controllers || !selection) {
|
||||
await context.editor.showWarningMessage('Cody Fixups: Failed to start.')
|
||||
return null
|
||||
}
|
||||
|
||||
// TODO: Remove dependency on human input and use input box only
|
||||
const humanInput =
|
||||
humanChatInput ||
|
||||
(await context.editor.showInputBox('Ask Cody to edit your code, or use /chat to ask a question.')) ||
|
||||
''
|
||||
|
||||
const taskID = controllers.task.add(humanInput, selection)
|
||||
if ((!humanInput && !selection.selectedText.trim()) || !taskID) {
|
||||
await context.editor.showWarningMessage(
|
||||
'Cody Fixups: Failed to start due to missing instruction with empty selection.'
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const quarterFileContext = Math.floor(MAX_CURRENT_FILE_TOKENS / 4)
|
||||
if (truncateText(selection.selectedText, quarterFileContext * 2) !== selection.selectedText) {
|
||||
const msg = "The amount of text selected exceeds Cody's current capacity."
|
||||
await context.editor.showWarningMessage(msg)
|
||||
return null
|
||||
}
|
||||
|
||||
// Reconstruct Cody's prompt using user's context
|
||||
// Replace placeholders in reverse order to avoid collisions if a placeholder occurs in the input
|
||||
const promptText = NonStop.prompt
|
||||
.replace('{humanInput}', truncateText(humanInput, MAX_HUMAN_INPUT_TOKENS))
|
||||
.replace('{responseMultiplexerPrompt}', context.responseMultiplexer.prompt())
|
||||
.replace('{truncateFollowingText}', truncateText(selection.followingText, quarterFileContext))
|
||||
.replace('{selectedText}', selection.selectedText)
|
||||
.replace('{truncateTextStart}', truncateTextStart(selection.precedingText, quarterFileContext))
|
||||
.replace('{fileName}', selection.fileName)
|
||||
|
||||
context.responseMultiplexer.sub(
|
||||
'selection',
|
||||
new BufferedBotResponseSubscriber(async content => {
|
||||
// TODO Handles LLM output
|
||||
// TODO Replace the selected text with the suggested replacement
|
||||
// Mark the task as done
|
||||
controllers.task.stop(taskID)
|
||||
if (!content) {
|
||||
await context.editor.showWarningMessage('Cody did not suggest any replacement.')
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
return Promise.resolve(
|
||||
new Interaction(
|
||||
{
|
||||
speaker: 'human',
|
||||
text: promptText,
|
||||
displayText: 'Cody Fixups: ' + humanInput,
|
||||
},
|
||||
{
|
||||
speaker: 'assistant',
|
||||
prefix: 'Check your document for updates from Cody.',
|
||||
},
|
||||
this.getContextMessages(selection.selectedText, context.codebaseContext)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
// Get context from editor
|
||||
private async getContextMessages(text: string, codebaseContext: CodebaseContext): Promise<ContextMessage[]> {
|
||||
const contextMessages: ContextMessage[] = await codebaseContext.getContextMessages(text, {
|
||||
numCodeResults: 12,
|
||||
numTextResults: 3,
|
||||
})
|
||||
return contextMessages
|
||||
}
|
||||
|
||||
// Prompt Templates
|
||||
public static readonly prompt = `
|
||||
This is part of the file {fileName}. The part of the file I have selected is enclosed with the <selection> tags. You are helping me to work on that part as my coding assistant.
|
||||
Follow the instructions in the selected part along with the additional instruction provide below to produce a rewritten replacement for only the selected part.
|
||||
Put the rewritten replacement inside <selection> tags. I only want to see the code within <selection>.
|
||||
Do not move code from outside the selection into the selection in your reply.
|
||||
Do not remove code inside the <selection> tags that might be being used by the code outside the <selection> tags.
|
||||
Do not enclose replacement code with tags other than the <selection> tags.
|
||||
Do not enclose your answer with any markdown.
|
||||
Only return provide me the replacement <selection> and nothing else.
|
||||
If it doesn't make sense, you do not need to provide <selection>.
|
||||
|
||||
\`\`\`
|
||||
{truncateTextStart}<selection>{selectedText}</selection>{truncateFollowingText}
|
||||
\`\`\`
|
||||
|
||||
Additional Instruction:
|
||||
- {humanInput}
|
||||
- {responseMultiplexerPrompt}
|
||||
`
|
||||
}
|
||||
@ -28,6 +28,7 @@ export type RecipeID =
|
||||
| 'release-notes'
|
||||
| 'inline-chat'
|
||||
| 'next-questions'
|
||||
| 'non-stop'
|
||||
| 'optimize-code'
|
||||
|
||||
export interface Recipe {
|
||||
|
||||
@ -7,11 +7,12 @@ export interface Configuration {
|
||||
debugFilter: RegExp | null
|
||||
debugVerbose: boolean
|
||||
useContext: ConfigurationUseContext
|
||||
customHeaders: Record<string, string>
|
||||
experimentalSuggest: boolean
|
||||
experimentalChatPredictions: boolean
|
||||
experimentalInline: boolean
|
||||
experimentalGuardrails: boolean
|
||||
customHeaders: Record<string, string>
|
||||
experimentalNonStop: boolean
|
||||
}
|
||||
|
||||
export interface ConfigurationWithAccessToken extends Configuration {
|
||||
|
||||
@ -21,12 +21,22 @@ export interface ActiveTextEditorVisibleContent {
|
||||
revision?: string
|
||||
}
|
||||
|
||||
export interface InlineController {
|
||||
interface VsCodeInlineController {
|
||||
selection: ActiveTextEditorSelection | null
|
||||
}
|
||||
|
||||
interface VsCodeTaskContoller {
|
||||
add(input: string, selection: ActiveTextEditorSelection): string | null
|
||||
stop(taskID: string): void
|
||||
}
|
||||
|
||||
export interface ActiveTextEditorViewControllers {
|
||||
inline: VsCodeInlineController
|
||||
task: VsCodeTaskContoller
|
||||
}
|
||||
|
||||
export interface Editor {
|
||||
controller?: InlineController
|
||||
controllers?: ActiveTextEditorViewControllers
|
||||
getWorkspaceRootPath(): string | null
|
||||
getActiveTextEditor(): ActiveTextEditor | null
|
||||
getActiveTextEditorSelection(): ActiveTextEditorSelection | null
|
||||
|
||||
5
client/cody/BUILD.bazel
generated
5
client/cody/BUILD.bazel
generated
@ -53,6 +53,10 @@ ts_project(
|
||||
"src/local-app-detector.ts",
|
||||
"src/log.ts",
|
||||
"src/main.ts",
|
||||
"src/non-stop/FixupTask.ts",
|
||||
"src/non-stop/TaskController.ts",
|
||||
"src/non-stop/TaskViewProvider.ts",
|
||||
"src/non-stop/utils.ts",
|
||||
"src/rg.ts",
|
||||
"src/services/CodeLensProvider.ts",
|
||||
"src/services/DecorationProvider.ts",
|
||||
@ -120,6 +124,7 @@ ts_project(
|
||||
"src/completions/provider.test.ts",
|
||||
"src/configuration.test.ts",
|
||||
"src/keyword-context/local-keyword-context-fetcher.test.ts",
|
||||
"src/non-stop/utils.test.ts",
|
||||
"src/services/InlineAssist.test.ts",
|
||||
"test/fixtures/mock-server.ts",
|
||||
],
|
||||
|
||||
@ -90,9 +90,23 @@
|
||||
"id": "cody.chat",
|
||||
"name": "Chat",
|
||||
"visibility": "visible"
|
||||
},
|
||||
{
|
||||
"id": "cody.fixup.tree.view",
|
||||
"name": "Fixups",
|
||||
"when": "cody.nonstop.fixups.enabled && cody.activated",
|
||||
"icon": "cody.svg",
|
||||
"contextualTitle": "Fixups"
|
||||
}
|
||||
]
|
||||
},
|
||||
"viewsWelcome": [
|
||||
{
|
||||
"view": "cody.fixup.tree.view",
|
||||
"contents": "No pending Cody fixups",
|
||||
"when": "cody.nonstop.fixups.enabled"
|
||||
}
|
||||
],
|
||||
"commands": [
|
||||
{
|
||||
"command": "cody.recipe.optimize-code",
|
||||
@ -192,7 +206,43 @@
|
||||
"command": "cody.guardrails.debug",
|
||||
"title": "Cody: Guardrails Debug Attribution",
|
||||
"enablement": "config.cody.debug && config.cody.experimental.guardrails && editorHasSelection"
|
||||
}
|
||||
},
|
||||
{
|
||||
"command": "cody.recipe.non-stop",
|
||||
"title": "Cody: Fixup (Experimental)",
|
||||
"icon": "resources/cody.png",
|
||||
"enablement": "cody.nonstop.fixups.enabled"
|
||||
},
|
||||
{
|
||||
"command": "cody.fixup.open",
|
||||
"title": "Cody: Go to Fixup",
|
||||
"enablement": "cody.nonstop.fixups.enabled",
|
||||
"icon": "$(file-code)"
|
||||
},
|
||||
{
|
||||
"command": "cody.fixup.apply",
|
||||
"title": "Cody: Apply fixup",
|
||||
"enablement": "!cody.fixup.view.isEmpty",
|
||||
"icon": "$(check)"
|
||||
},
|
||||
{
|
||||
"command": "cody.fixup.apply-all",
|
||||
"title": "Cody: Apply all fixups",
|
||||
"enablement": "!cody.fixup.view.isEmpty",
|
||||
"icon": "$(check-all)"
|
||||
},
|
||||
{
|
||||
"command": "cody.fixup.apply-by-file",
|
||||
"title": "Cody: Apply fixups to selected directory",
|
||||
"enablement": "!cody.fixup.view.isEmpty",
|
||||
"icon": "$(check-all)"
|
||||
},
|
||||
{
|
||||
"command": "cody.fixup.diff",
|
||||
"title": "Cody: Show diff for fixup",
|
||||
"enablement": "!cody.fixup.view.isEmpty",
|
||||
"icon": "$(diff)"
|
||||
}
|
||||
],
|
||||
"keybindings": [
|
||||
{
|
||||
@ -205,6 +255,12 @@
|
||||
"key": "ctrl+alt+/",
|
||||
"mac": "ctrl+alt+/",
|
||||
"when": "cody.activated && editorTextFocus && !editorReadonly"
|
||||
},
|
||||
{
|
||||
"command": "cody.recipe.non-stop",
|
||||
"key": "ctrl+shift+v",
|
||||
"mac": "shift+cmd+v",
|
||||
"when": "cody.activated && editorTextFocus && !editorReadonly"
|
||||
}
|
||||
],
|
||||
"submenus": [
|
||||
@ -272,6 +328,22 @@
|
||||
"command": "cody.comment.load",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "cody.fixup.apply",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "cody.fixup.apply-all",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "cody.fixup.apply-by-file",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "cody.fixup.diff",
|
||||
"when": "false"
|
||||
},
|
||||
{
|
||||
"command": "cody.manual-completions",
|
||||
"when": "config.cody.experimental.suggestions"
|
||||
@ -340,6 +412,19 @@
|
||||
"command": "cody.history",
|
||||
"when": "view == cody.chat && cody.activated",
|
||||
"group": "navigation@2"
|
||||
},
|
||||
{
|
||||
"command": "cody.fixup.apply-all",
|
||||
"when": "cody.nonstop.fixups.enabled && view == cody.fixup.tree.view && cody.activated",
|
||||
"group": "navigation"
|
||||
}
|
||||
],
|
||||
"editor/title": [
|
||||
{
|
||||
"command": "cody.recipe.non-stop",
|
||||
"when": "cody.nonstop.fixups.enabled && cody.activated",
|
||||
"group": "navigation",
|
||||
"visibility": "visible"
|
||||
}
|
||||
],
|
||||
"comments/commentThread/context": [
|
||||
@ -365,6 +450,24 @@
|
||||
"group": "inline@2",
|
||||
"when": "cody.activated && commentController =~ /^cody-inline/ && cody.reply.pending"
|
||||
}
|
||||
],
|
||||
"view/item/context": [
|
||||
{
|
||||
"command": "cody.fixup.apply-by-file",
|
||||
"when": "cody.nonstop.fixups.enabled && view == cody.fixup.tree.view && cody.activated && viewItem == fsPath",
|
||||
"enable": "cody.fixup.filesWithApplicableFixups",
|
||||
"group": "inline"
|
||||
},
|
||||
{
|
||||
"command": "cody.fixup.apply",
|
||||
"when": "cody.nonstop.fixups.enabled && view == cody.fixup.tree.view && cody.activated && viewItem == task",
|
||||
"group": "inline@2"
|
||||
},
|
||||
{
|
||||
"command": "cody.fixup.diff",
|
||||
"when": "cody.nonstop.fixups.enabled && view == cody.fixup.tree.view && cody.activated && viewItem == task",
|
||||
"group": "inline@1"
|
||||
}
|
||||
]
|
||||
},
|
||||
"configuration": {
|
||||
@ -431,6 +534,11 @@
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"cody.experimental.nonStop": {
|
||||
"order": 9,
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"cody.debug.enable": {
|
||||
"order": 99,
|
||||
"type": "boolean",
|
||||
|
||||
@ -24,6 +24,7 @@ import { VSCodeEditor } from '../editor/vscode-editor'
|
||||
import { logEvent } from '../event-logger'
|
||||
import { LocalAppDetector } from '../local-app-detector'
|
||||
import { debug } from '../log'
|
||||
import { FixupTask } from '../non-stop/FixupTask'
|
||||
import { LocalStorage } from '../services/LocalStorageProvider'
|
||||
import { CODY_ACCESS_TOKEN_SECRET, SecretStorage } from '../services/SecretStorageProvider'
|
||||
import { TestSupport } from '../test-support'
|
||||
@ -305,7 +306,7 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp
|
||||
// TODO(keegancsmith) guardrails may be slow, we need to make this async update the interaction.
|
||||
highlightedDisplayText = await this.guardrailsAnnotateAttributions(highlightedDisplayText)
|
||||
this.transcript.addAssistantResponse(text || '', highlightedDisplayText)
|
||||
this.editor.controller.reply(highlightedDisplayText)
|
||||
this.editor.controllers.inline.reply(highlightedDisplayText)
|
||||
}
|
||||
void this.onCompletionEnd()
|
||||
},
|
||||
@ -793,6 +794,14 @@ export class ChatViewProvider implements vscode.WebviewViewProvider, vscode.Disp
|
||||
return this.transcript.toChat()
|
||||
}
|
||||
|
||||
public fixupTasksForTesting(testing: TestSupport): FixupTask[] {
|
||||
if (!testing) {
|
||||
console.error('used ForTesting method without test support object')
|
||||
return []
|
||||
}
|
||||
return this.editor.controllers.task.getTasks()
|
||||
}
|
||||
|
||||
public dispose(): void {
|
||||
for (const disposable of this.disposables) {
|
||||
disposable.dispose()
|
||||
|
||||
@ -11,6 +11,7 @@ import { GitHistory } from '@sourcegraph/cody-shared/src/chat/recipes/git-log'
|
||||
import { ImproveVariableNames } from '@sourcegraph/cody-shared/src/chat/recipes/improve-variable-names'
|
||||
import { InlineChat } from '@sourcegraph/cody-shared/src/chat/recipes/inline-chat'
|
||||
import { NextQuestions } from '@sourcegraph/cody-shared/src/chat/recipes/next-questions'
|
||||
import { NonStop } from '@sourcegraph/cody-shared/src/chat/recipes/non-stop'
|
||||
import { OptimizeCode } from '@sourcegraph/cody-shared/src/chat/recipes/optimize-code'
|
||||
import { Recipe, RecipeID } from '@sourcegraph/cody-shared/src/chat/recipes/recipe'
|
||||
import { TranslateToLanguage } from '@sourcegraph/cody-shared/src/chat/recipes/translate'
|
||||
@ -47,6 +48,7 @@ function init(): void {
|
||||
new NextQuestions(),
|
||||
new ContextSearch(),
|
||||
new ReleaseNotes(),
|
||||
new NonStop(),
|
||||
new OptimizeCode(),
|
||||
]
|
||||
|
||||
|
||||
@ -15,6 +15,7 @@ describe('getConfiguration', () => {
|
||||
experimentalChatPredictions: false,
|
||||
experimentalGuardrails: false,
|
||||
experimentalInline: false,
|
||||
experimentalNonStop: false,
|
||||
customHeaders: {},
|
||||
debugEnable: false,
|
||||
debugVerbose: false,
|
||||
@ -45,6 +46,8 @@ describe('getConfiguration', () => {
|
||||
return true
|
||||
case 'cody.experimental.inline':
|
||||
return true
|
||||
case 'cody.experimental.nonStop':
|
||||
return true
|
||||
case 'cody.debug.enable':
|
||||
return true
|
||||
case 'cody.debug.verbose':
|
||||
@ -68,6 +71,7 @@ describe('getConfiguration', () => {
|
||||
experimentalChatPredictions: true,
|
||||
experimentalGuardrails: true,
|
||||
experimentalInline: true,
|
||||
experimentalNonStop: true,
|
||||
debugEnable: true,
|
||||
debugVerbose: true,
|
||||
debugFilter: /.*/,
|
||||
|
||||
@ -41,6 +41,7 @@ export function getConfiguration(config: Pick<vscode.WorkspaceConfiguration, 'ge
|
||||
experimentalChatPredictions: config.get('cody.experimental.chatPredictions', isTesting),
|
||||
experimentalInline: config.get('cody.experimental.inline', isTesting),
|
||||
experimentalGuardrails: config.get('cody.experimental.guardrails', isTesting),
|
||||
experimentalNonStop: config.get('cody.experimental.nonStop', isTesting),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -8,10 +8,16 @@ import {
|
||||
} from '@sourcegraph/cody-shared/src/editor'
|
||||
import { SURROUNDING_LINES } from '@sourcegraph/cody-shared/src/prompt/constants'
|
||||
|
||||
import { TaskController } from '../non-stop/TaskController'
|
||||
import { InlineController } from '../services/InlineController'
|
||||
|
||||
export class VSCodeEditor implements Editor {
|
||||
constructor(public controller: InlineController) {}
|
||||
constructor(
|
||||
public controllers: {
|
||||
inline: InlineController
|
||||
task: TaskController
|
||||
}
|
||||
) {}
|
||||
|
||||
public getWorkspaceRootPath(): string | null {
|
||||
const uri = vscode.window.activeTextEditor?.document?.uri
|
||||
@ -40,7 +46,7 @@ export class VSCodeEditor implements Editor {
|
||||
}
|
||||
|
||||
public getActiveTextEditorSelection(): ActiveTextEditorSelection | null {
|
||||
if (this.controller.isInProgress) {
|
||||
if (this.controllers.inline.isInProgress) {
|
||||
return null
|
||||
}
|
||||
const activeEditor = this.getActiveTextEditorInstance()
|
||||
@ -115,8 +121,8 @@ export class VSCodeEditor implements Editor {
|
||||
|
||||
public async replaceSelection(fileName: string, selectedText: string, replacement: string): Promise<void> {
|
||||
const activeEditor = this.getActiveTextEditorInstance()
|
||||
if (this.controller.isInProgress) {
|
||||
await this.controller.replaceSelection(replacement)
|
||||
if (this.controllers.inline.isInProgress) {
|
||||
await this.controllers.inline.replaceSelection(replacement)
|
||||
return
|
||||
}
|
||||
if (!activeEditor || vscode.workspace.asRelativePath(activeEditor.document.uri.fsPath) !== fileName) {
|
||||
|
||||
@ -12,6 +12,7 @@ import { getConfiguration, getFullConfig } from './configuration'
|
||||
import { VSCodeEditor } from './editor/vscode-editor'
|
||||
import { logEvent, updateEventLogger } from './event-logger'
|
||||
import { configureExternalServices } from './external-services'
|
||||
import { TaskController } from './non-stop/TaskController'
|
||||
import { getRgPath } from './rg'
|
||||
import { GuardrailsProvider } from './services/GuardrailsProvider'
|
||||
import { InlineController } from './services/InlineController'
|
||||
@ -79,7 +80,10 @@ const register = async (
|
||||
const commentController = new InlineController(context.extensionPath)
|
||||
disposables.push(commentController.get())
|
||||
|
||||
const editor = new VSCodeEditor(commentController)
|
||||
const taskController = new TaskController()
|
||||
const controllers = { inline: commentController, task: taskController }
|
||||
|
||||
const editor = new VSCodeEditor(controllers)
|
||||
const workspaceConfig = vscode.workspace.getConfiguration()
|
||||
const config = getConfiguration(workspaceConfig)
|
||||
|
||||
@ -252,6 +256,16 @@ const register = async (
|
||||
})
|
||||
)
|
||||
}
|
||||
// Register task view and non-stop cody command when feature flag is on
|
||||
if (initialConfig.experimentalNonStop || process.env.CODY_TESTING === 'true') {
|
||||
disposables.push(vscode.window.registerTreeDataProvider('cody.fixup.tree.view', taskController.getTaskView()))
|
||||
disposables.push(
|
||||
vscode.commands.registerCommand('cody.recipe.non-stop', async () => {
|
||||
await chatProvider.executeRecipe('non-stop', '', false)
|
||||
})
|
||||
)
|
||||
await vscode.commands.executeCommand('setContext', 'cody.nonstop.fixups.enabled', true)
|
||||
}
|
||||
|
||||
return {
|
||||
disposable: vscode.Disposable.from(...disposables),
|
||||
|
||||
80
client/cody/src/non-stop/FixupTask.ts
Normal file
80
client/cody/src/non-stop/FixupTask.ts
Normal file
@ -0,0 +1,80 @@
|
||||
import * as vscode from 'vscode'
|
||||
|
||||
import { ActiveTextEditorSelection } from '@sourcegraph/cody-shared/src/editor'
|
||||
|
||||
import { debug } from '../log'
|
||||
|
||||
import { CodyTaskState } from './utils'
|
||||
|
||||
// TODO(bee): Create CodeLens for each task
|
||||
// TODO(bee): Create decorator for each task
|
||||
// TODO(dpc): Add listener for document change to track range
|
||||
export class FixupTask {
|
||||
public id: string
|
||||
private outputChannel = debug
|
||||
public selectionRange: vscode.Range
|
||||
public state = CodyTaskState.idle
|
||||
public readonly documentUri: vscode.Uri
|
||||
|
||||
constructor(
|
||||
public readonly instruction: string,
|
||||
public selection: ActiveTextEditorSelection,
|
||||
public readonly editor: vscode.TextEditor
|
||||
) {
|
||||
this.id = Date.now().toString(36).replace(/\d+/g, '')
|
||||
this.selectionRange = editor.selection
|
||||
this.documentUri = editor.document.uri
|
||||
this.queue()
|
||||
}
|
||||
/**
|
||||
* Set latest state for task and then update icon accordingly
|
||||
*/
|
||||
private setState(state: CodyTaskState): void {
|
||||
if (this.state !== CodyTaskState.error) {
|
||||
this.state = state
|
||||
}
|
||||
}
|
||||
|
||||
public start(): void {
|
||||
this.setState(CodyTaskState.pending)
|
||||
this.output(`Task #${this.id} is currently being processed...`)
|
||||
}
|
||||
|
||||
public stop(): void {
|
||||
this.setState(CodyTaskState.done)
|
||||
this.output(`Task #${this.id} has been completed...`)
|
||||
}
|
||||
|
||||
public error(text: string = ''): void {
|
||||
this.setState(CodyTaskState.error)
|
||||
this.output(`Error for Task #${this.id} - ` + text)
|
||||
}
|
||||
|
||||
public apply(): void {
|
||||
this.setState(CodyTaskState.applying)
|
||||
this.output(`Task #${this.id} is being applied...`)
|
||||
}
|
||||
|
||||
private queue(): void {
|
||||
this.setState(CodyTaskState.queued)
|
||||
this.output(`Task #${this.id} has been added to the queue successfully...`)
|
||||
}
|
||||
/**
|
||||
* Print output to the VS Code Output Channel under Cody AI by Sourcegraph
|
||||
*/
|
||||
private output(text: string): void {
|
||||
this.outputChannel('Cody Fixups:', text)
|
||||
}
|
||||
/**
|
||||
* Return latest selection
|
||||
*/
|
||||
public getSelection(): ActiveTextEditorSelection | null {
|
||||
return this.selection
|
||||
}
|
||||
/**
|
||||
* Return latest selection range
|
||||
*/
|
||||
public getSelectionRange(): vscode.Range | vscode.Selection {
|
||||
return this.selectionRange
|
||||
}
|
||||
}
|
||||
171
client/cody/src/non-stop/TaskController.ts
Normal file
171
client/cody/src/non-stop/TaskController.ts
Normal file
@ -0,0 +1,171 @@
|
||||
import * as vscode from 'vscode'
|
||||
|
||||
import { ActiveTextEditorSelection } from '@sourcegraph/cody-shared/src/editor'
|
||||
|
||||
import { FixupTask } from './FixupTask'
|
||||
import { TaskViewProvider, FixupTaskTreeItem } from './TaskViewProvider'
|
||||
import { CodyTaskState } from './utils'
|
||||
|
||||
type taskID = string
|
||||
|
||||
// This class acts as the factory for Fixup Tasks and handles communication between the Tree View and editor
|
||||
export class TaskController {
|
||||
private tasks = new Map<taskID, FixupTask>()
|
||||
private readonly taskViewProvider: TaskViewProvider
|
||||
|
||||
private _disposables: vscode.Disposable[] = []
|
||||
|
||||
constructor() {
|
||||
// Register commands
|
||||
this._disposables.push(vscode.commands.registerCommand('cody.fixup.open', id => this.showThisFixup(id)))
|
||||
this._disposables.push(
|
||||
vscode.commands.registerCommand('cody.fixup.apply', treeItem => this.applyFixup(treeItem))
|
||||
)
|
||||
this._disposables.push(
|
||||
vscode.commands.registerCommand('cody.fixup.apply-by-file', treeItem => this.applyDirFixups(treeItem))
|
||||
)
|
||||
this._disposables.push(
|
||||
vscode.commands.registerCommand('cody.fixup.apply-all', treeItem => this.applyAllFixups(treeItem))
|
||||
)
|
||||
this._disposables.push(vscode.commands.registerCommand('cody.fixup.diff', treeItem => this.showDiff(treeItem)))
|
||||
// Start the fixup tree view provider
|
||||
this.taskViewProvider = new TaskViewProvider()
|
||||
}
|
||||
|
||||
// Adds a new task to the list of tasks
|
||||
// Then mark it as pending before sending it to the tree view for tree item creation
|
||||
public add(input: string, selection: ActiveTextEditorSelection): string | null {
|
||||
const editor = vscode.window.activeTextEditor
|
||||
if (!editor) {
|
||||
void vscode.window.showInformationMessage('No active editor found...')
|
||||
return null
|
||||
}
|
||||
|
||||
// Create a task and then mark it as start
|
||||
const task = new FixupTask(input, selection, editor)
|
||||
task.start()
|
||||
void vscode.commands.executeCommand('setContext', 'cody.fixup.running', true)
|
||||
// Save states of the task
|
||||
this.tasks.set(task.id, task)
|
||||
this.taskViewProvider.setTreeItem(task)
|
||||
return task.id
|
||||
}
|
||||
|
||||
// Replaces content of the file before mark the task as done
|
||||
// Then update the tree view with the new task state
|
||||
public stop(taskID: taskID): void {
|
||||
const task = this.tasks.get(taskID)
|
||||
if (!task) {
|
||||
return
|
||||
}
|
||||
task.stop()
|
||||
// Save states of the task
|
||||
this.tasks.set(task.id, task)
|
||||
this.taskViewProvider.setTreeItem(task)
|
||||
void vscode.commands.executeCommand('setContext', 'cody.fixup.running', false)
|
||||
this.getFilesWithApplicableFixups()
|
||||
}
|
||||
|
||||
private getFilesWithApplicableFixups(): string[] {
|
||||
const filePaths: string[] = []
|
||||
for (const task of this.tasks.values()) {
|
||||
if (task.state === CodyTaskState.done) {
|
||||
filePaths.push(task.documentUri.fsPath)
|
||||
}
|
||||
}
|
||||
void vscode.commands.executeCommand('setContext', 'cody.fixup.filesWithApplicableFixups', filePaths.length > 0)
|
||||
return filePaths
|
||||
}
|
||||
|
||||
// Open fsPath at the selected line in editor on tree item click
|
||||
private showThisFixup(taskID: taskID): void {
|
||||
const task = this.tasks.get(taskID)
|
||||
if (!task) {
|
||||
void vscode.window.showInformationMessage('No fixup was found...')
|
||||
return
|
||||
}
|
||||
// Create vscode Uri from task uri and selection range
|
||||
void vscode.window.showTextDocument(task.documentUri, { selection: task.selectionRange })
|
||||
}
|
||||
|
||||
// TODO: Add support for applying fixup
|
||||
// Placeholder function for applying fixup
|
||||
private applyFixup(treeItem?: FixupTaskTreeItem): void {
|
||||
void vscode.window.showInformationMessage(`Applying fixup for task #${treeItem?.id} is not implemented yet...`)
|
||||
|
||||
if (treeItem?.contextValue === 'task' && treeItem?.id) {
|
||||
const task = this.tasks.get(treeItem.id)
|
||||
task?.apply()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Add support for applying fixup to all tasks in a directory
|
||||
// Placeholder function for applying fixup to all tasks in a directory
|
||||
private applyDirFixups(treeItem: FixupTaskTreeItem): void {
|
||||
void vscode.window.showInformationMessage('Applying fixups to a directory is not implemented yet...')
|
||||
|
||||
if (treeItem?.contextValue === 'fsPath') {
|
||||
for (const task of this.tasks.values()) {
|
||||
if (task.documentUri.fsPath === treeItem.fsPath && task.state === CodyTaskState.done) {
|
||||
task.apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.getFilesWithApplicableFixups()
|
||||
}
|
||||
|
||||
// TODO: Add support for applying all fixup
|
||||
// Placeholder function for applying fixup
|
||||
private applyAllFixups(treeItem?: FixupTaskTreeItem): void {
|
||||
void vscode.window.showInformationMessage('Applying fixup for all tasks is not implemented yet...')
|
||||
|
||||
// Apply all fixups
|
||||
for (const task of this.tasks.values()) {
|
||||
if (task.state === CodyTaskState.done) {
|
||||
task.apply()
|
||||
}
|
||||
}
|
||||
// Clear task view after applying fixups
|
||||
// TODO Catch errors
|
||||
this.reset()
|
||||
}
|
||||
|
||||
// TODO: Add support for showing diff
|
||||
// Placeholder function for showing diff
|
||||
private showDiff(treeItem: FixupTaskTreeItem): string {
|
||||
if (!treeItem?.id) {
|
||||
void vscode.window.showInformationMessage('No fixup was found...')
|
||||
return ''
|
||||
}
|
||||
const task = this.tasks.get(treeItem?.id)
|
||||
// TODO: Implement diff view
|
||||
void vscode.window.showInformationMessage(`Diff view for task #${task?.id} is not implemented yet...`)
|
||||
return task?.selection.selectedText || ''
|
||||
}
|
||||
|
||||
public getTaskView(): TaskViewProvider {
|
||||
return this.taskViewProvider
|
||||
}
|
||||
|
||||
public getTasks(): FixupTask[] {
|
||||
return Array.from(this.tasks.values())
|
||||
}
|
||||
|
||||
private reset(): void {
|
||||
this.tasks = new Map<taskID, FixupTask>()
|
||||
this.taskViewProvider.reset()
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose the disposables
|
||||
*/
|
||||
public dispose(): void {
|
||||
this.taskViewProvider.dispose()
|
||||
for (const disposable of this._disposables) {
|
||||
disposable.dispose()
|
||||
}
|
||||
this._disposables = []
|
||||
}
|
||||
}
|
||||
173
client/cody/src/non-stop/TaskViewProvider.ts
Normal file
173
client/cody/src/non-stop/TaskViewProvider.ts
Normal file
@ -0,0 +1,173 @@
|
||||
import * as vscode from 'vscode'
|
||||
|
||||
import { FixupTask } from './FixupTask'
|
||||
import { CodyTaskState, fixupTaskIcon, getFileNameAfterLastDash } from './utils'
|
||||
|
||||
type taskID = string
|
||||
type fileName = string
|
||||
export class TaskViewProvider implements vscode.TreeDataProvider<FixupTaskTreeItem> {
|
||||
/**
|
||||
* Tree items are mapped by fsPath to taskID
|
||||
*/
|
||||
// Add type alias for Map key
|
||||
private treeNodes = new Map<fileName, FixupTaskTreeItem>()
|
||||
private treeItems = new Map<taskID, FixupTaskTreeItem>()
|
||||
|
||||
private _disposables: vscode.Disposable[] = []
|
||||
private _onDidChangeTreeData = new vscode.EventEmitter<FixupTaskTreeItem | undefined | void>()
|
||||
public readonly onDidChangeTreeData = this._onDidChangeTreeData.event
|
||||
|
||||
constructor() {
|
||||
void vscode.commands.executeCommand('setContext', 'cody.fixup.view.isEmpty', true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Refresh the tree view to get the latest data
|
||||
*/
|
||||
public refresh(): void {
|
||||
void vscode.commands.executeCommand('setContext', 'cody.fixup.view.isEmpty', this.treeNodes.size === 0)
|
||||
this._onDidChangeTreeData.fire()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get parents items first
|
||||
* Then returns children items for each parent item
|
||||
*/
|
||||
public getChildren(element?: FixupTaskTreeItem): FixupTaskTreeItem[] {
|
||||
if (element && element.contextValue === 'fsPath') {
|
||||
return [...this.treeItems.values()].filter(item => item.fsPath === element.fsPath)
|
||||
}
|
||||
|
||||
return [...this.treeNodes.values()]
|
||||
}
|
||||
|
||||
/**
|
||||
* Create tree item based on provided task
|
||||
*/
|
||||
public setTreeItem(task: FixupTask): void {
|
||||
const treeItem = new FixupTaskTreeItem(task.instruction, task)
|
||||
this.treeItems.set(task.id, treeItem)
|
||||
|
||||
// Add fsPath to treeNodes
|
||||
const treeNode = this.treeNodes.get(task.selection.fileName) || new FixupTaskTreeItem(task.selection.fileName)
|
||||
treeNode.addChildren(task.id, task.state)
|
||||
this.treeNodes.set(task.selection.fileName, treeNode)
|
||||
|
||||
this.refresh()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get individual tree item
|
||||
*/
|
||||
public getTreeItem(element: FixupTaskTreeItem): FixupTaskTreeItem {
|
||||
return element
|
||||
}
|
||||
|
||||
public removeTreeItemByID(taskID: taskID): void {
|
||||
const task = this.treeItems.get(taskID)
|
||||
if (!task) {
|
||||
return
|
||||
}
|
||||
this.treeItems.delete(taskID)
|
||||
}
|
||||
|
||||
public removeTreeItemsByFileName(fileName: fileName): void {
|
||||
const task = this.treeNodes.get(fileName)
|
||||
if (!task) {
|
||||
return
|
||||
}
|
||||
this.treeNodes.delete(fileName)
|
||||
}
|
||||
|
||||
/**
|
||||
* Empty the tree view
|
||||
*/
|
||||
public reset(): void {
|
||||
this.treeNodes = new Map<fileName, FixupTaskTreeItem>()
|
||||
this.treeItems = new Map<taskID, FixupTaskTreeItem>()
|
||||
this.refresh()
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose the disposables
|
||||
*/
|
||||
public dispose(): void {
|
||||
this.reset()
|
||||
for (const disposable of this._disposables) {
|
||||
disposable.dispose()
|
||||
}
|
||||
this._disposables = []
|
||||
}
|
||||
}
|
||||
|
||||
export class FixupTaskTreeItem extends vscode.TreeItem {
|
||||
private state: CodyTaskState = CodyTaskState.idle
|
||||
public fsPath: string
|
||||
|
||||
// state for parent node
|
||||
private failed = new Set<string>()
|
||||
private tasks = new Set<string>()
|
||||
|
||||
constructor(label: string, task?: FixupTask) {
|
||||
super(label)
|
||||
if (!task) {
|
||||
this.fsPath = label
|
||||
this.tooltip = label
|
||||
this.label = getFileNameAfterLastDash(label)
|
||||
this.contextValue = 'fsPath'
|
||||
this.collapsibleState = vscode.TreeItemCollapsibleState.Expanded
|
||||
this.description = '0 fixups'
|
||||
return
|
||||
}
|
||||
this.state = task.state
|
||||
this.id = task.id
|
||||
this.fsPath = task.selection.fileName
|
||||
this.resourceUri = task.documentUri
|
||||
this.contextValue = 'task'
|
||||
this.collapsibleState = vscode.TreeItemCollapsibleState.None
|
||||
this.tooltip = new vscode.MarkdownString(`Task #${task.id}: ${task.instruction}`, true)
|
||||
this.command = { command: 'cody.fixup.open', title: 'Go to File', arguments: [task.id] }
|
||||
|
||||
this.updateIconPath()
|
||||
}
|
||||
|
||||
// For parent node to track children states
|
||||
public addChildren(taskID: string, state: CodyTaskState): void {
|
||||
if (this.contextValue !== 'fsPath') {
|
||||
return
|
||||
}
|
||||
this.tasks.add(taskID)
|
||||
this.description = this.makeNodeDescription(state)
|
||||
}
|
||||
|
||||
private makeNodeDescription(state: CodyTaskState): string {
|
||||
const tasksSize = this.tasks.size
|
||||
const failedSize = this.failed.size
|
||||
let text = `${tasksSize} ${tasksSize > 1 ? 'fixups' : 'fixup'}`
|
||||
let ready = tasksSize - failedSize
|
||||
|
||||
switch (state) {
|
||||
case CodyTaskState.pending:
|
||||
text += ', 1 running'
|
||||
ready--
|
||||
break
|
||||
case CodyTaskState.applying:
|
||||
text += ', 1 applying'
|
||||
ready--
|
||||
break
|
||||
}
|
||||
if (failedSize > 0) {
|
||||
text += `, ${failedSize} failed`
|
||||
}
|
||||
if (ready > 0) {
|
||||
text += `, ${ready} ready`
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
private updateIconPath(): void {
|
||||
const icon = fixupTaskIcon[this.state].icon
|
||||
const mode = fixupTaskIcon[this.state].id
|
||||
this.iconPath = new vscode.ThemeIcon(icon, new vscode.ThemeColor(mode))
|
||||
}
|
||||
}
|
||||
20
client/cody/src/non-stop/utils.test.ts
Normal file
20
client/cody/src/non-stop/utils.test.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { getFileNameAfterLastDash } from './utils'
|
||||
|
||||
// Test for getFileNameAfterLastDash
|
||||
describe('getFileNameAfterLastDash', () => {
|
||||
test('gets the last part of the file path after the last slash', () => {
|
||||
const filePath = '/path/to/file.txt'
|
||||
const fileName = 'file.txt'
|
||||
expect(getFileNameAfterLastDash(filePath)).toEqual(fileName)
|
||||
})
|
||||
test('get file name when there is no slash', () => {
|
||||
const filePath = 'file.txt'
|
||||
const fileName = 'file.txt'
|
||||
expect(getFileNameAfterLastDash(filePath)).toEqual(fileName)
|
||||
})
|
||||
test('get file name when there is no extension', () => {
|
||||
const filePath = 'file'
|
||||
const fileName = 'file'
|
||||
expect(getFileNameAfterLastDash(filePath)).toEqual(fileName)
|
||||
})
|
||||
})
|
||||
54
client/cody/src/non-stop/utils.ts
Normal file
54
client/cody/src/non-stop/utils.ts
Normal file
@ -0,0 +1,54 @@
|
||||
export enum CodyTaskState {
|
||||
'idle' = 0,
|
||||
'queued' = 1,
|
||||
'pending' = 2,
|
||||
'done' = 3,
|
||||
'applying' = 4,
|
||||
'error' = 5,
|
||||
}
|
||||
|
||||
export type CodyTaskIcon = {
|
||||
[key in CodyTaskState]: {
|
||||
id: string
|
||||
icon: string
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Icon for each task state
|
||||
*/
|
||||
export const fixupTaskIcon: CodyTaskIcon = {
|
||||
[CodyTaskState.idle]: {
|
||||
id: 'idle',
|
||||
icon: 'smiley',
|
||||
},
|
||||
[CodyTaskState.pending]: {
|
||||
id: 'pending',
|
||||
icon: 'sync~spin',
|
||||
},
|
||||
[CodyTaskState.done]: {
|
||||
id: 'done',
|
||||
icon: 'issue-closed',
|
||||
},
|
||||
[CodyTaskState.error]: {
|
||||
id: 'error',
|
||||
icon: 'stop',
|
||||
},
|
||||
[CodyTaskState.queued]: {
|
||||
id: 'queue',
|
||||
icon: 'clock',
|
||||
},
|
||||
[CodyTaskState.applying]: {
|
||||
id: 'applying',
|
||||
icon: 'sync~spin',
|
||||
},
|
||||
}
|
||||
/**
|
||||
* Get the last part of the file path after the last slash
|
||||
*/
|
||||
export function getFileNameAfterLastDash(filePath: string): string {
|
||||
const lastDashIndex = filePath.lastIndexOf('/')
|
||||
if (lastDashIndex === -1) {
|
||||
return filePath
|
||||
}
|
||||
return filePath.slice(lastDashIndex + 1)
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import { ChatMessage } from '@sourcegraph/cody-shared/src/chat/transcript/messages'
|
||||
|
||||
import { ChatViewProvider } from './chat/ChatViewProvider'
|
||||
import { FixupTask } from './non-stop/FixupTask'
|
||||
|
||||
// A one-slot channel which lets readers block on a value being
|
||||
// available from a writer. Tests use this to wait for the
|
||||
@ -41,4 +42,8 @@ export class TestSupport {
|
||||
public async chatTranscript(): Promise<ChatMessage[]> {
|
||||
return (await this.chatViewProvider.get()).transcriptForTesting(this)
|
||||
}
|
||||
|
||||
public async fixupTasks(): Promise<FixupTask[]> {
|
||||
return (await this.chatViewProvider.get()).fixupTasksForTesting(this)
|
||||
}
|
||||
}
|
||||
|
||||
1
client/cody/test/e2e/BUILD.bazel
generated
1
client/cody/test/e2e/BUILD.bazel
generated
@ -13,6 +13,7 @@ ts_project(
|
||||
"helpers.ts",
|
||||
"history.test.ts",
|
||||
"inline-assist.test.ts",
|
||||
"task-controller.test.ts",
|
||||
],
|
||||
tsconfig = "//client/cody:tsconfig",
|
||||
deps = [
|
||||
|
||||
@ -15,8 +15,13 @@ test('requires a valid auth token and allows logouts', async ({ page, sidebar })
|
||||
await sidebar.getByRole('textbox', { name: 'Access Token (docs)' }).fill(VALID_TOKEN)
|
||||
await sidebar.getByRole('button', { name: 'Sign In' }).click()
|
||||
|
||||
// Collapse the task tree view
|
||||
await page.getByRole('button', { name: 'Fixups Section' }).click()
|
||||
|
||||
await expect(sidebar.getByText("Hello! I'm Cody.")).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'Chat Section' }).hover()
|
||||
|
||||
await page.click('[aria-label="Cody: Settings"]')
|
||||
await sidebar.getByRole('button', { name: 'Logout' }).click()
|
||||
|
||||
|
||||
@ -3,12 +3,17 @@ import { Frame, Locator, Page, expect } from '@playwright/test'
|
||||
import { SERVER_URL, VALID_TOKEN } from '../fixtures/mock-server'
|
||||
|
||||
// Sign into Cody with valid auth from the sidebar
|
||||
export const sidebarSignin = async (sidebar: Frame): Promise<void> => {
|
||||
export const sidebarSignin = async (page: Page, sidebar: Frame): Promise<void> => {
|
||||
await sidebar.getByRole('textbox', { name: 'Sourcegraph Instance URL' }).fill(SERVER_URL)
|
||||
await sidebar.getByRole('textbox', { name: 'Access Token (docs)' }).fill(VALID_TOKEN)
|
||||
await sidebar.getByRole('button', { name: 'Sign In' }).click()
|
||||
|
||||
// Collapse the task tree view
|
||||
await page.getByRole('button', { name: 'Fixups Section' }).click()
|
||||
|
||||
await expect(sidebar.getByText("Hello! I'm Cody.")).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'Chat Section' }).hover()
|
||||
}
|
||||
|
||||
// Selector for the Explorer button in the sidebar that would match on Mac and Linux
|
||||
|
||||
@ -10,8 +10,13 @@ test('checks for the chat history and new session', async ({ page, sidebar }) =>
|
||||
await sidebar.getByRole('textbox', { name: 'Access Token (docs)' }).fill(VALID_TOKEN)
|
||||
await sidebar.getByRole('button', { name: 'Sign In' }).click()
|
||||
|
||||
// Collapse the task tree view
|
||||
await page.getByRole('button', { name: 'Fixups Section' }).click()
|
||||
|
||||
await expect(sidebar.getByText("Hello! I'm Cody.")).toBeVisible()
|
||||
|
||||
await page.getByRole('button', { name: 'Chat Section' }).hover()
|
||||
|
||||
await page.click('[aria-label="Cody: Chat History"]')
|
||||
await expect(sidebar.getByText('Chat History')).toBeVisible()
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import { test } from './helpers'
|
||||
|
||||
test('start a fixup job from inline assist with valid auth', async ({ page, sidebar }) => {
|
||||
// Sign into Cody
|
||||
await sidebarSignin(sidebar)
|
||||
await sidebarSignin(page, sidebar)
|
||||
|
||||
// Open the Explorer view from the sidebar
|
||||
await sidebarExplorer(page).click()
|
||||
|
||||
67
client/cody/test/e2e/task-controller.test.ts
Normal file
67
client/cody/test/e2e/task-controller.test.ts
Normal file
@ -0,0 +1,67 @@
|
||||
import { expect } from '@playwright/test'
|
||||
|
||||
import { sidebarExplorer, sidebarSignin } from './common'
|
||||
import { test } from './helpers'
|
||||
|
||||
test('task tree view for non-stop cody', async ({ page, sidebar }) => {
|
||||
// Sign into Cody
|
||||
await sidebarSignin(page, sidebar)
|
||||
|
||||
// Open the Explorer view from the sidebar
|
||||
await sidebarExplorer(page).click()
|
||||
|
||||
// Select the second files from the tree view, which is the index.html file
|
||||
await page.locator('.monaco-highlighted-label').nth(2).click()
|
||||
|
||||
// Bring the cody sidebar to the foreground
|
||||
await page.click('[aria-label="Sourcegraph Cody"]')
|
||||
|
||||
// Expand the task tree view
|
||||
await page.getByRole('button', { name: 'Fixups Section' }).click()
|
||||
|
||||
// Open the command palette by clicking on the Cody Icon
|
||||
// Expect to see fail to start because no text was selected
|
||||
await page.getByRole('button', { name: /Cody: Fixup.*/ }).click()
|
||||
await expect(page.getByText(/^Cody Fixups: Failed to start.*/)).toBeVisible()
|
||||
|
||||
// Find the text hello cody, and then highlight the text
|
||||
await page.getByText('<title>Hello Cody</title>').click()
|
||||
|
||||
// Hightlight the whole line
|
||||
await page.keyboard.down('Shift')
|
||||
await page.keyboard.press('ArrowDown')
|
||||
|
||||
// Open the command palette by clicking on the Cody Icon
|
||||
await page.getByRole('button', { name: /Cody: Fixup.*/ }).click()
|
||||
// Type in the instruction for fixup
|
||||
await page.keyboard.type('replace hello with goodbye')
|
||||
// Press enter to submit the fixup
|
||||
await page.keyboard.press('Enter')
|
||||
|
||||
// Expect to see the fixup instruction in the task tree view
|
||||
await expect(page.getByText('1 fixup, 1 ready')).toBeVisible()
|
||||
await expect(page.getByText('No pending Cody fixups')).not.toBeVisible()
|
||||
|
||||
// Diff view button
|
||||
await page.locator('a').filter({ hasText: 'replace hello with goodbye' }).click()
|
||||
await page.getByRole('button', { name: 'Cody: Show diff for fixup' }).click()
|
||||
await expect(page.getByText(/^Diff view for task.*/)).toBeVisible()
|
||||
|
||||
// Apply fixup button on Click
|
||||
await page.locator('a').filter({ hasText: 'replace hello with goodbye' }).click()
|
||||
await page.getByRole('button', { name: 'Cody: Apply fixup' }).click()
|
||||
await expect(page.getByText(/^Applying fixup for task.*/)).toBeVisible()
|
||||
|
||||
// Close the file tab and then clicking on the tree item again should open the file again
|
||||
await page.getByRole('button', { name: /^Close.*/ }).click()
|
||||
await expect(page.getByText('<title>Hello Cody</title>')).not.toBeVisible()
|
||||
await page.locator('a').filter({ hasText: 'replace hello with goodbye' }).click()
|
||||
await expect(page.getByText('<title>Hello Cody</title>')).toBeVisible()
|
||||
|
||||
// Collapse the task tree view
|
||||
await page.getByRole('button', { name: 'Fixups Section' }).click()
|
||||
await expect(page.getByText('replace hello with good bye')).not.toBeVisible()
|
||||
|
||||
// The chat view should be visible again
|
||||
await expect(sidebar.getByText(/^Check your doc.*/)).toBeVisible()
|
||||
})
|
||||
18
client/cody/test/fixtures/mock-server.ts
vendored
18
client/cody/test/fixtures/mock-server.ts
vendored
@ -1,5 +1,17 @@
|
||||
import express from 'express'
|
||||
|
||||
// create interface for the request
|
||||
interface MockRequest {
|
||||
headers: {
|
||||
authorization: string
|
||||
}
|
||||
body: {
|
||||
messages: {
|
||||
text: string
|
||||
}[]
|
||||
}
|
||||
}
|
||||
|
||||
const SERVER_PORT = 49300
|
||||
|
||||
export const SERVER_URL = 'http://localhost:49300'
|
||||
@ -17,7 +29,11 @@ export async function run<T>(around: () => Promise<T>): Promise<T> {
|
||||
|
||||
app.post('/.api/completions/stream', (req, res) => {
|
||||
// TODO: Filter streaming response
|
||||
const response = req.body.messages[2].text.includes('<selection>') ? responses.fixup : responses.chat
|
||||
// TODO: Handle multiple messages
|
||||
// Ideas from Dom - see if we could put something in the test request itself where we tell it what to respond with
|
||||
// or have a method on the server to send a set response the next time it sees a trigger word in the request.
|
||||
const request = req as MockRequest
|
||||
const response = request.body.messages[2].text.includes('<selection>') ? responses.fixup : responses.chat
|
||||
res.send(`event: completion\ndata: {"completion": ${JSON.stringify(response)}}\n\nevent: done\ndata: {}\n\n`)
|
||||
})
|
||||
|
||||
|
||||
1
client/cody/test/integration/BUILD.bazel
generated
1
client/cody/test/integration/BUILD.bazel
generated
@ -28,6 +28,7 @@ ts_project(
|
||||
"index.ts",
|
||||
"main.ts",
|
||||
"recipes.test.ts",
|
||||
"task-controller.test.ts",
|
||||
],
|
||||
tsconfig = ":tsconfig",
|
||||
deps = [
|
||||
|
||||
@ -5,6 +5,7 @@ import * as vscode from 'vscode'
|
||||
import { ChatMessage } from '@sourcegraph/cody-shared/src/chat/transcript/messages'
|
||||
|
||||
import { ExtensionApi } from '../../src/extension-api'
|
||||
import { FixupTask } from '../../src/non-stop/FixupTask'
|
||||
import * as mockServer from '../fixtures/mock-server'
|
||||
|
||||
/**
|
||||
@ -25,7 +26,7 @@ export async function beforeIntegrationTest(): Promise<void> {
|
||||
// Configure extension.
|
||||
const config = vscode.workspace.getConfiguration()
|
||||
await config.update('cody.serverEndpoint', mockServer.SERVER_URL)
|
||||
await ensureExecuteCommand('cody.set-access-token', ['test-token'])
|
||||
await ensureExecuteCommand('cody.set-access-token', [mockServer.VALID_TOKEN])
|
||||
}
|
||||
|
||||
/**
|
||||
@ -76,3 +77,21 @@ export async function getTranscript(index: number): Promise<ChatMessage> {
|
||||
assert.ok(transcript)
|
||||
return transcript[index]
|
||||
}
|
||||
|
||||
export async function getFixupTasks(): Promise<FixupTask[]> {
|
||||
const api = getExtensionAPI()
|
||||
const testSupport = api.exports.testing
|
||||
assert.ok(testSupport)
|
||||
|
||||
let fixups: FixupTask[] | undefined
|
||||
|
||||
await waitUntil(async () => {
|
||||
if (!api.isActive || !api.exports.testing) {
|
||||
return false
|
||||
}
|
||||
fixups = await getExtensionAPI().exports.testing?.fixupTasks()
|
||||
return true
|
||||
})
|
||||
assert.ok(fixups)
|
||||
return fixups || []
|
||||
}
|
||||
|
||||
89
client/cody/test/integration/task-controller.test.ts
Normal file
89
client/cody/test/integration/task-controller.test.ts
Normal file
@ -0,0 +1,89 @@
|
||||
import * as assert from 'assert'
|
||||
|
||||
import * as vscode from 'vscode'
|
||||
|
||||
import { ChatViewProvider } from '../../src/chat/ChatViewProvider'
|
||||
|
||||
import { afterIntegrationTest, beforeIntegrationTest, getExtensionAPI, getFixupTasks, getTranscript } from './helpers'
|
||||
|
||||
async function getChatViewProvider(): Promise<ChatViewProvider> {
|
||||
const chatViewProvider = await getExtensionAPI().exports.testing?.chatViewProvider.get()
|
||||
assert.ok(chatViewProvider)
|
||||
return chatViewProvider
|
||||
}
|
||||
|
||||
suite('Cody Fixup Task Controller', function () {
|
||||
this.beforeEach(() => beforeIntegrationTest())
|
||||
this.afterEach(() => afterIntegrationTest())
|
||||
|
||||
let textEditor = vscode.window.activeTextEditor
|
||||
|
||||
// Run the non-stop recipe to create a new task before every test
|
||||
this.beforeEach(async () => {
|
||||
await vscode.commands.executeCommand('cody.chat.focus')
|
||||
const chatView = await getChatViewProvider()
|
||||
|
||||
// Open index.html
|
||||
assert.ok(vscode.workspace.workspaceFolders)
|
||||
|
||||
const indexUri = vscode.Uri.parse(`${vscode.workspace.workspaceFolders[0].uri.toString()}/index.html`)
|
||||
textEditor = await vscode.window.showTextDocument(indexUri)
|
||||
|
||||
// Select the "title" tags to run the recipe on
|
||||
textEditor.selection = new vscode.Selection(6, 0, 7, 0)
|
||||
|
||||
// Brings up the vscode input box
|
||||
await chatView.executeRecipe('non-stop', 'Replace hello with goodbye', false)
|
||||
|
||||
// Check the chat transcript contains markdown
|
||||
const humanMessage = await getTranscript(0)
|
||||
|
||||
assert.match(humanMessage.displayText || '', /^Cody Fixups: Replace hello with goodbye/)
|
||||
assert.match((await getTranscript(1)).displayText || '', /^Check your document for updates from Cody/)
|
||||
})
|
||||
|
||||
test('task controller', async () => {
|
||||
if (textEditor === undefined) {
|
||||
assert.fail('editor is undefined')
|
||||
}
|
||||
// Check the Fixup Tasks from Task Controller contains the new task
|
||||
const tasks = await getFixupTasks()
|
||||
assert.strictEqual(tasks.length, 1)
|
||||
|
||||
assert.match(tasks[0].instruction, /^Replace hello with goodbye/)
|
||||
|
||||
// Get selection text from editor whch should match <title>Hello Cody</title>
|
||||
// TODO: Update to <title>Goodbye Cody</title> after we have implemented the replace method. Right now we are marking it as done
|
||||
const selectionText = textEditor.document.getText(textEditor.selection).trim()
|
||||
assert.match(selectionText, /^<title>Hello Cody<\/title>/)
|
||||
|
||||
// Run the apply command should remove all tasks from the task controller
|
||||
await vscode.commands.executeCommand('cody.fixup.apply')
|
||||
assert.strictEqual((await getFixupTasks()).length, 1)
|
||||
})
|
||||
|
||||
test('show this fixup', async () => {
|
||||
// Check the Fixup Tasks from Task Controller contains the new task
|
||||
const tasks = await getFixupTasks()
|
||||
assert.strictEqual(tasks.length, 2)
|
||||
|
||||
// Switch to a different file
|
||||
const mainJavaUri = vscode.Uri.parse(`${vscode.workspace.workspaceFolders?.[0].uri.toString()}/Main.java`)
|
||||
await vscode.workspace.openTextDocument(mainJavaUri)
|
||||
|
||||
// Run show command to open fixup file with range selected
|
||||
await vscode.commands.executeCommand('cody.fixup.open', tasks[0].id)
|
||||
|
||||
const newEditor = vscode.window.activeTextEditor
|
||||
assert.strictEqual(newEditor, textEditor)
|
||||
})
|
||||
|
||||
test('apply fixups', async () => {
|
||||
const tasks = await getFixupTasks()
|
||||
assert.strictEqual(tasks.length, 3)
|
||||
|
||||
// Run the apply command should remove all tasks from the task controller
|
||||
await vscode.commands.executeCommand('cody.fixup.apply-all')
|
||||
assert.strictEqual((await getFixupTasks()).length, 0)
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user