Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(chat-ui): add smart apply to chat panel #3112

Merged
merged 47 commits into from
Oct 26, 2024
Merged
Show file tree
Hide file tree
Changes from 41 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
7a7b3ed
feat(chat-ui): add smart apply functionality to chat panel
Sma1lboy Sep 10, 2024
4b848cc
to: smart apply in editor also pass language id
Sma1lboy Sep 11, 2024
6241339
feat(tabby-agent): adding providing best fit range for smart apply fu…
Sma1lboy Sep 11, 2024
b2a370a
to(vscode): request fit line range
Sma1lboy Sep 11, 2024
9e6d1ca
feat: add onSmartApplyInEditor function to ChatSideBar component
Sma1lboy Sep 11, 2024
7360a15
to(tabby-agent): adding provideSmartApplyRequest base on chatEdit
Sma1lboy Sep 11, 2024
f4f91a2
feat(vscode): add smart apply functionality to ChatViewProvider
Sma1lboy Sep 11, 2024
36424d1
feat: remove onSmartApplyInEditor from ChatPanel and ChatSideBar comp…
Sma1lboy Sep 12, 2024
331d0d4
feat(tabby-agent): add smart apply functionality to generate-smart-ap…
Sma1lboy Sep 16, 2024
4946bec
Merge branch 'main' into feat-smart-apply
Sma1lboy Sep 16, 2024
7490a0d
refactor: Update parameter options in TabbyAgent.ts to include lineRa…
Sma1lboy Sep 17, 2024
f032434
feat: add support for indentInfo in SmartApplyCodeParams.undefined
Sma1lboy Sep 17, 2024
69e67e7
docs: update smart apply instructions with indentation details.undefined
Sma1lboy Sep 17, 2024
d5d845f
fix(tabby-agent): update documentation and remove unused imports
Sma1lboy Sep 17, 2024
1643079
chore: remove unnecessary prompt filesundefined
Sma1lboy Sep 17, 2024
5d11479
fix(lsp): add cancellation token handling for ChatLineRangeSmartApply…
Sma1lboy Sep 19, 2024
7405d54
Merge branch 'main' into origin-feat-smart-apply
Sma1lboy Sep 26, 2024
55220c3
rebase: rebase chat web view client api to webviewhelper
Sma1lboy Sep 26, 2024
bd06cbd
fix(ui): add support for wrapping long lines in CodeBlock component
Sma1lboy Sep 26, 2024
9b1553d
Merge branch 'main' into feat-smart-apply
Sma1lboy Oct 13, 2024
8f8b94d
refactor: update chatPanelViewProvider constructor parameters in Comm…
Sma1lboy Oct 13, 2024
94aeb37
refactor: Update imports and remove unused code in chat inline edit f…
Sma1lboy Oct 13, 2024
a3744a1
feat: add fuzzyApplyRange function for applying ranges with fuzzy mat…
Sma1lboy Oct 13, 2024
ef3e35b
refactor(Commands): Modify the creation of ChatPanelViewProvider to i…
Sma1lboy Oct 13, 2024
d630c6a
refactor(chat): add SmartApplyFeature to the chat functionality.
Sma1lboy Oct 13, 2024
7b8e1f3
refactor(chat): remove smart apply code
Sma1lboy Oct 13, 2024
a2c6531
refactor(vscode): remove provide line range method, also update new p…
Sma1lboy Oct 13, 2024
8734ae9
refactor: rename fuzzyApplyRange.ts to SmartRange.ts and update funct…
Sma1lboy Oct 13, 2024
b1b964f
refactor(chat): update SmartApplyFeature to handle apply range with f…
Sma1lboy Oct 13, 2024
22fafad
refactor: update chat feature to include revealing editor range funct…
Sma1lboy Oct 13, 2024
798b173
refactor(client): remove global chat status
Sma1lboy Oct 13, 2024
90b5354
refactor(vscode): separate the workspace lsp server request from the …
Sma1lboy Oct 13, 2024
a8d4ee6
chore: remove unused logger
Sma1lboy Oct 13, 2024
ff177df
Merge branch 'main' into feat-smart-apply
Sma1lboy Oct 15, 2024
df05e27
refactor(smart-apply): still using diff llms to apply code
Sma1lboy Oct 16, 2024
88db1bc
docs: add comments for explaining the return value of getSmartApplyRa…
Sma1lboy Oct 17, 2024
645ed2f
refactor(chat): refactor SmartApply.ts with revealEditorRange functio…
Sma1lboy Oct 17, 2024
905df91
chore: update code insertion guidelines.
Sma1lboy Oct 17, 2024
5ee4490
chore: remove RevealEditorRangeRequest and using existing LSP API
Sma1lboy Oct 22, 2024
58fc87e
chore: move smartApply to single section
Sma1lboy Oct 22, 2024
dd7f007
chore: remove unused dynamic feature interface
Sma1lboy Oct 22, 2024
30cd9f8
chore: remove lsp client ShowDocRequest implementation
Sma1lboy Oct 24, 2024
a39a4d9
Merge branch 'main' into feat-smart-apply
Sma1lboy Oct 24, 2024
ab3843e
chore: adding rule avoid generate \n for first line for smart apply p…
Sma1lboy Oct 25, 2024
61d7bbd
fix: update code style for pr 3112.
icycodes Oct 25, 2024
3d4e2bb
fix(vscode): update smart apply api.
icycodes Oct 25, 2024
19d5609
Merge pull request #2 from icycodes/fix-pr-3113-update-code-style
Sma1lboy Oct 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions clients/tabby-agent/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"@types/fast-levenshtein": "^0.0.4",
"@types/fs-extra": "^11.0.1",
"@types/glob": "^7.2.0",
"@types/js-levenshtein": "^1.1.3",
"@types/mocha": "^10.0.1",
"@types/node": "18.x",
"@types/object-hash": "^3.0.0",
Expand All @@ -68,6 +69,7 @@
"file-stream-rotator": "^1.0.0",
"fs-extra": "^11.1.1",
"glob": "^7.2.0",
"js-levenshtein": "^1.1.6",
"jwt-decode": "^3.1.2",
"lru-cache": "^9.1.1",
"mac-ca": "^2.0.3",
Expand Down
335 changes: 335 additions & 0 deletions clients/tabby-agent/src/chat/SmartApply.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,335 @@
import {
CancellationToken,
Connection,
Location,
Range,
ShowDocumentParams,
TextDocuments,
} from "vscode-languageserver";
import type { Feature } from "../feature";
import {
ChatEditDocumentTooLongError,
ChatEditMutexError,
ChatFeatureNotAvailableError,
ServerCapabilities,
SmartApplyCodeRequest,
SmartApplyCodeParams,
} from "../protocol";
import { Configurations } from "../config";
import { TabbyApiClient } from "../http/tabbyApiClient";
import cryptoRandomString from "crypto-random-string";
import { getLogger } from "../logger";
import { readResponseStream, showDocument } from "./utils";
import { TextDocument } from "vscode-languageserver-textdocument";
import { getSmartApplyRange } from "./SmartRange";
import { Edit } from "./inlineEdit";
const logger = getLogger("ChatEditProvider");

export class SmartApplyFeature implements Feature {
private lspConnection: Connection | undefined = undefined;
private mutexAbortController: AbortController | undefined = undefined;
constructor(
private readonly configurations: Configurations,
private readonly tabbyApiClient: TabbyApiClient,
private readonly documents: TextDocuments<TextDocument>,
) {}

initialize(connection: Connection): ServerCapabilities | Promise<ServerCapabilities> {
this.lspConnection = connection;
connection.onRequest(SmartApplyCodeRequest.type, async (params, token) => {
return this.provideSmartApplyEdit(params, token);
});

return {};
}
initialized?(): void | Promise<void> {
//nothing
}
shutdown?(): void | Promise<void> {
//nothing
}

async provideSmartApplyEdit(params: SmartApplyCodeParams, token: CancellationToken): Promise<boolean> {
logger.info("Getting document");
const document = this.documents.get(params.location.uri);
if (!document) {
logger.info("Document not found, returning false");
return false;
}
if (!this.lspConnection) {
logger.info("LSP connection lost.");
return false;
}

if (this.mutexAbortController && !this.mutexAbortController.signal.aborted) {
logger.warn("Another smart edit is already in progress");
throw {
name: "ChatEditMutexError",
message: "Another smart edit is already in progress",
} as ChatEditMutexError;
}
this.mutexAbortController = new AbortController();
logger.info("mutex abort status: " + (this.mutexAbortController === undefined));
token.onCancellationRequested(() => this.mutexAbortController?.abort());

let applyRange = getSmartApplyRange(document, params.applyCode);
//if cannot find range, lets use backend LLMs
if (!applyRange) {
if (!this.tabbyApiClient.isChatApiAvailable) {
return false;
}
applyRange = await provideSmartApplyLineRange(
document,
params.applyCode,
this.tabbyApiClient,
this.configurations,
);
}

if (!applyRange) {
return false;
}

try {
//reveal editor range
const revealEditorRangeParams: ShowDocumentParams = {
uri: params.location.uri,
selection: {
start: applyRange.range.start,
end: applyRange.range.end,
},
takeFocus: true,
};
await showDocument(revealEditorRangeParams, this.lspConnection);
} catch (error) {
logger.warn("cline not support reveal range");
}

try {
await provideSmartApplyEditLLM(
{
uri: params.location.uri,
range: {
start: applyRange.range.start,
end: { line: applyRange.range.end.line + 1, character: 0 },
},
},
params.applyCode,
applyRange.action === "insert" ? true : false,
document,
this.lspConnection,
this.tabbyApiClient,
this.configurations,
this.mutexAbortController,
() => {
this.mutexAbortController = undefined;
},
);
return true;
} catch (error) {
logger.error("Error applying smart edit:", error);
return false;
} finally {
logger.info("Resetting mutex abort controller");
this.mutexAbortController = undefined;
}
}
}

export async function provideSmartApplyLineRange(
document: TextDocument,
applyCodeBlock: string,
tabbyApiClient: TabbyApiClient,
configurations: Configurations,
): Promise<{ range: Range; action: "insert" | "replace" } | undefined> {
if (!document) {
return undefined;
}
if (!tabbyApiClient.isChatApiAvailable()) {
throw {
name: "ChatFeatureNotAvailableError",
message: "Chat feature not available",
} as ChatFeatureNotAvailableError;
}

const documentText = document
.getText()
.split("\n")
.map((line, idx) => `${idx + 1} | ${line}`)
.join("\n");

const config = configurations.getMergedConfig();
const promptTemplate = config.chat.provideSmartApplyLineRange.promptTemplate;

const messages: { role: "user"; content: string }[] = [
{
role: "user",
content: promptTemplate.replace(/{{document}}|{{applyCode}}/g, (pattern: string) => {
switch (pattern) {
case "{{document}}":
return documentText;
case "{{applyCode}}":
return applyCodeBlock;
default:
return "";
}
}),
},
];

try {
const readableStream = await tabbyApiClient.fetchChatStream({
messages,
model: "",
stream: true,
});

if (!readableStream) {
return undefined;
}

let response = "";
for await (const chunk of readableStream) {
response += chunk;
}

const regex = /<GENERATEDCODE>(.*?)<\/GENERATEDCODE>/s;
const match = response.match(regex);
if (match && match[1]) {
response = match[1].trim();
}

const range = response.split("-");
if (range.length !== 2) {
return undefined;
}

const startLine = parseInt(range[0] ?? "0", 10) - 1;
const endLine = parseInt(range[1] ?? "0", 10) - 1;

return {
range: {
start: { line: startLine < 0 ? 0 : startLine, character: 0 },
end: { line: endLine < 0 ? 0 : endLine, character: Number.MAX_SAFE_INTEGER },
},
action: startLine == endLine ? "insert" : "replace",
};
} catch (error) {
return undefined;
}
}

export async function provideSmartApplyEditLLM(
location: Location,
applyCode: string,
insertMode: boolean,
document: TextDocument,
lspConnection: Connection,
tabbyApiClient: TabbyApiClient,
configurations: Configurations,
mutexAbortController: AbortController,
onResetMutex: () => void,
): Promise<boolean> {
if (!document) {
logger.warn("Document not found");
return false;
}
if (!lspConnection) {
logger.warn("LSP connection failed");
return false;
}

if (!tabbyApiClient.isChatApiAvailable()) {
throw {
name: "ChatFeatureNotAvailableError",
message: "Chat feature not available",
} as ChatFeatureNotAvailableError;
}

const config = configurations.getMergedConfig();
const documentText = document.getText();
const selection = {
start: document.offsetAt(location.range.start),
end: document.offsetAt(location.range.end),
};
const selectedDocumentText = documentText.substring(selection.start, selection.end);

logger.info("current selectedDoc: " + selectedDocumentText);

if (selection.end - selection.start > config.chat.edit.documentMaxChars) {
throw { name: "ChatEditDocumentTooLongError", message: "Document too long" } as ChatEditDocumentTooLongError;
}

const promptTemplate = config.chat.provideSmartApply.promptTemplate;

// Extract the selected text and the surrounding context
let documentPrefix = documentText.substring(0, selection.start);
let documentSuffix = documentText.substring(selection.end);
if (documentText.length > config.chat.edit.documentMaxChars) {
const charsRemain = config.chat.edit.documentMaxChars - selectedDocumentText.length;
if (documentPrefix.length < charsRemain / 2) {
documentSuffix = documentSuffix.substring(0, charsRemain - documentPrefix.length);
} else if (documentSuffix.length < charsRemain / 2) {
documentPrefix = documentPrefix.substring(documentPrefix.length - charsRemain + documentSuffix.length);
} else {
documentPrefix = documentPrefix.substring(documentPrefix.length - charsRemain / 2);
documentSuffix = documentSuffix.substring(0, charsRemain / 2);
}
}

const messages: { role: "user"; content: string }[] = [
{
role: "user",
content: promptTemplate.replace(/{{document}}|{{code}}/g, (pattern: string) => {
switch (pattern) {
case "{{document}}":
return selectedDocumentText;
case "{{code}}":
return applyCode || "";
default:
return "";
}
}),
},
];

try {
const readableStream = await tabbyApiClient.fetchChatStream({
messages,
model: "",
stream: true,
});

if (!readableStream) {
return false;
}
const editId = "tabby-" + cryptoRandomString({ length: 6, type: "alphanumeric" });
const currentEdit: Edit = {
id: editId,
location: location,
languageId: document.languageId,
originalText: selectedDocumentText,
editedRange: insertMode
? { start: location.range.start, end: location.range.end }
: { start: location.range.start, end: location.range.end },
editedText: "",
comments: "",
buffer: "",
state: "editing",
};

await readResponseStream(
readableStream,
lspConnection,
currentEdit,
mutexAbortController,
onResetMutex,
config.chat.edit.responseDocumentTag,
config.chat.edit.responseCommentTag,
);

return true;
} catch (error) {
return false;
}
}
45 changes: 45 additions & 0 deletions clients/tabby-agent/src/chat/SmartRange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import levenshtein from "js-levenshtein";
import { Position, Range } from "vscode-languageserver-protocol";
import { TextDocument } from "vscode-languageserver-textdocument";

//return [start, end] close interval 0-based range
export function getSmartApplyRange(
document: TextDocument,
snippet: string,
): { range: Range; action: "insert" | "replace" } | undefined {
const applyRange = fuzzyApplyRange(document, snippet);
if (!applyRange) {
return undefined;
}
//insert mode
if (applyRange.range.start.line === applyRange.range.end.line || document.getText().trim() === "") {
return { range: applyRange.range, action: "insert" };
}
return { range: applyRange.range, action: "replace" };
}

export function fuzzyApplyRange(document: TextDocument, snippet: string): { range: Range; score: number } | null {
const lines = document.getText().split("\n");
const snippetLines = snippet.split("\n");

let [minDistance, index] = [Number.MAX_SAFE_INTEGER, 0];
for (let i = 0; i <= lines.length - snippetLines.length; i++) {
const window = lines.slice(i, i + snippetLines.length).join("\n");
const distance = levenshtein(window, snippet);
if (minDistance >= distance) {
minDistance = distance;
index = i;
}
}

if (minDistance === Number.MAX_SAFE_INTEGER && index === 0) {
return null;
}

const startLine = index;
const endLine = index + snippetLines.length - 1;
const start: Position = { line: startLine, character: 0 };
const end: Position = { line: endLine, character: lines[endLine]?.length || 0 };

return { range: { start, end }, score: minDistance };
}
Loading