Skip to content

Commit 2c46e91

Browse files
catrielmullerCopilothassoncs
authored
Inline Assist - Setting section & Support all providers (#1396)
* feat: Ghost Service Settings Section and Provider functionallity * feat: Add Translations and changeset * Update src/shared/WebviewMessage.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/shared/ExtensionMessage.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/core/webview/ClineProvider.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update src/core/webview/ClineProvider.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * test: ghost provider performance test (#1432) * Update .changeset/orange-hats-like.md Co-authored-by: Chris Hasson <hassoncs@gmail.com> * Update webview-ui/src/i18n/locales/en/kilocode.json Co-authored-by: Chris Hasson <hassoncs@gmail.com> * refactor: update translations --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Chris Hasson <hassoncs@gmail.com>
1 parent 1789b4d commit 2c46e91

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

45 files changed

+735
-44
lines changed

.changeset/orange-hats-like.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"kilo-code": patch
3+
---
4+
5+
Adds new Settings page for Inline Assist
6+
7+
You can now select the provider you'd like to use for `Inline Assist` commands

packages/types/src/global-settings.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import { telemetrySettingsSchema } from "./telemetry.js"
1414
import { modeConfigSchema } from "./mode.js"
1515
import { customModePromptsSchema, customSupportPromptsSchema } from "./mode.js"
1616
import { languagesSchema } from "./vscode.js"
17+
import { ghostServiceSettingsSchema } from "./kilocode.js" // kilocode_change
1718

1819
/**
1920
* GlobalSettings
@@ -116,6 +117,7 @@ export const globalSettingsSchema = z.object({
116117
enhancementApiConfigId: z.string().optional(),
117118
autocompleteApiConfigId: z.string().optional(), // kilocode_change
118119
commitMessageApiConfigId: z.string().optional(), // kilocode_change
120+
ghostServiceSettings: ghostServiceSettingsSchema, // kilocode_change
119121
historyPreviewCollapsed: z.boolean().optional(),
120122
profileThresholds: z.record(z.string(), z.number()).optional(),
121123
hasOpenedModeSelector: z.boolean().optional(),
@@ -222,6 +224,7 @@ export const EVALS_SETTINGS: RooCodeSettings = {
222224
soundEnabled: false,
223225
soundVolume: 0.5,
224226
systemNotificationsEnabled: true, // kilocode_change
227+
ghostServiceSettings: {}, // kilocode_change
225228

226229
terminalOutputLineLimit: 500,
227230
terminalShellIntegrationTimeout: 30000,

packages/types/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ export * from "./terminal.js"
2020
export * from "./tool.js"
2121
export * from "./type-fu.js"
2222
export * from "./vscode.js"
23+
export * from "./kilocode.js" // kilocode_change
2324
export * from "./todo.js"

packages/types/src/kilocode.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { z } from "zod"
2+
3+
export const ghostServiceSettingsSchema = z
4+
.object({
5+
apiConfigId: z.string().optional(),
6+
})
7+
.optional()
8+
9+
export type GhostServiceSettings = z.infer<typeof ghostServiceSettingsSchema>

src/__mocks__/vscode.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ const mockDisposable = {
1010
}
1111

1212
const mockUri = {
13-
file: (path) => ({ fsPath: path, path, scheme: "file" }),
14-
parse: (path) => ({ fsPath: path, path, scheme: "file" }),
13+
file: (path) => ({ fsPath: path, path, scheme: "file", toString: () => path }),
14+
parse: (path) => ({ fsPath: path, path, scheme: "file", toString: () => path }),
1515
}
1616

1717
const mockRange = class {

src/core/webview/ClineProvider.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1468,6 +1468,7 @@ export class ClineProvider
14681468
maxConcurrentFileReads,
14691469
allowVeryLargeReads, // kilocode_change
14701470
autocompleteApiConfigId, // kilocode_change
1471+
ghostServiceSettings, // kilocode_changes
14711472
condensingApiConfigId,
14721473
customCondensingPrompt,
14731474
codebaseIndexConfig,
@@ -1579,6 +1580,7 @@ export class ClineProvider
15791580
sharingEnabled: sharingEnabled ?? false,
15801581
organizationAllowList,
15811582
autocompleteApiConfigId, // kilocode_change
1583+
ghostServiceSettings: ghostServiceSettings ?? {}, // kilocode_change
15821584
condensingApiConfigId,
15831585
customCondensingPrompt,
15841586
codebaseIndexModels: codebaseIndexModels ?? EMBEDDING_MODEL_PROFILES,
@@ -1723,6 +1725,7 @@ export class ClineProvider
17231725
enhancementApiConfigId: stateValues.enhancementApiConfigId,
17241726
commitMessageApiConfigId: stateValues.commitMessageApiConfigId, // kilocode_change
17251727
autocompleteApiConfigId: stateValues.autocompleteApiConfigId, // kilocode_change
1728+
ghostServiceSettings: stateValues.ghostServiceSettings ?? {}, // kilocode_change
17261729
experiments: stateValues.experiments ?? experimentDefault,
17271730
autoApprovalEnabled: stateValues.autoApprovalEnabled ?? true,
17281731
customModes,

src/core/webview/webviewMessageHandler.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
type GlobalState,
1414
type ClineMessage,
1515
TelemetryEventName,
16+
ghostServiceSettingsSchema, // kilocode_change
1617
} from "@roo-code/types"
1718
import { CloudService } from "@roo-code/cloud"
1819
import { TelemetryService } from "@roo-code/telemetry"
@@ -1341,6 +1342,16 @@ export const webviewMessageHandler = async (
13411342
await updateGlobalState("autocompleteApiConfigId", message.text)
13421343
await provider.postStateToWebview()
13431344
break
1345+
case "ghostServiceSettings":
1346+
if (!message.values) {
1347+
return
1348+
}
1349+
// Validate ghostServiceSettings structure
1350+
const ghostServiceSettings = ghostServiceSettingsSchema.parse(message.values)
1351+
await updateGlobalState("ghostServiceSettings", ghostServiceSettings)
1352+
await provider.postStateToWebview()
1353+
vscode.commands.executeCommand("kilo-code.ghost.reload")
1354+
break
13441355
// kilocode_change end
13451356
case "condensingApiConfigId":
13461357
await updateGlobalState("condensingApiConfigId", message.text)

src/services/autocomplete/__tests__/MockTextDocument.test.ts

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,58 @@
1-
import { describe, it, expect } from "vitest"
1+
import { describe, it, expect, vi, beforeEach } from "vitest"
22
import * as vscode from "vscode"
33
import { MockTextDocument } from "./MockTextDocument"
44

5+
vi.mock("vscode", () => ({
6+
EndOfLine: { LF: 1, CRLF: 2 },
7+
Uri: {
8+
parse: vi.fn((str) => ({
9+
fsPath: str,
10+
path: str,
11+
scheme: "file",
12+
})),
13+
},
14+
Position: class Position {
15+
constructor(
16+
public line: number,
17+
public character: number,
18+
) {}
19+
},
20+
Range: class Range {
21+
constructor(
22+
public start: vscode.Position,
23+
public end: vscode.Position,
24+
) {}
25+
},
26+
}))
27+
528
describe("MockTextDocument", () => {
29+
const testUri = vscode.Uri.parse("file:///test.ts")
30+
631
describe("constructor and basic properties", () => {
732
it("should create document from single line content", () => {
8-
const doc = new MockTextDocument("const x = 1")
33+
const doc = new MockTextDocument(testUri, "const x = 1")
934

1035
expect(doc.lineCount).toBe(1)
1136
expect(doc.getText()).toBe("const x = 1")
1237
})
1338

1439
it("should create document from multi-line content", () => {
1540
const content = "function test() {\n return true\n}"
16-
const doc = new MockTextDocument(content)
41+
const doc = new MockTextDocument(testUri, content)
1742

1843
expect(doc.lineCount).toBe(3)
1944
expect(doc.getText()).toBe(content)
2045
})
2146

2247
it("should handle empty content", () => {
23-
const doc = new MockTextDocument("")
48+
const doc = new MockTextDocument(testUri, "")
2449

2550
expect(doc.lineCount).toBe(1)
2651
expect(doc.getText()).toBe("")
2752
})
2853

2954
it("should handle content with only newlines", () => {
30-
const doc = new MockTextDocument("\n\n\n")
55+
const doc = new MockTextDocument(testUri, "\n\n\n")
3156

3257
expect(doc.lineCount).toBe(4)
3358
expect(doc.getText()).toBe("\n\n\n")
@@ -39,7 +64,7 @@ describe("MockTextDocument", () => {
3964
let doc: MockTextDocument
4065

4166
beforeEach(() => {
42-
doc = new MockTextDocument(multiLineContent)
67+
doc = new MockTextDocument(testUri, multiLineContent)
4368
})
4469

4570
it("should return full text when no range provided", () => {
@@ -94,7 +119,7 @@ describe("MockTextDocument", () => {
94119
let doc: MockTextDocument
95120

96121
beforeEach(() => {
97-
doc = new MockTextDocument(content)
122+
doc = new MockTextDocument(testUri, content)
98123
})
99124

100125
it("should return correct line information for first line", () => {
@@ -116,7 +141,7 @@ describe("MockTextDocument", () => {
116141
})
117142

118143
it("should return correct line information for whitespace-only line", () => {
119-
const docWithWhitespace = new MockTextDocument("line1\n \nline3")
144+
const docWithWhitespace = new MockTextDocument(testUri, "line1\n \nline3")
120145
const line = docWithWhitespace.lineAt(1)
121146

122147
expect(line.text).toBe(" ")
@@ -154,7 +179,7 @@ describe("MockTextDocument", () => {
154179

155180
describe("edge cases and special characters", () => {
156181
it("should handle tabs correctly", () => {
157-
const doc = new MockTextDocument("\tfunction test() {\n\t\treturn true\n\t}")
182+
const doc = new MockTextDocument(testUri, "\tfunction test() {\n\t\treturn true\n\t}")
158183

159184
expect(doc.lineCount).toBe(3)
160185

@@ -168,7 +193,7 @@ describe("MockTextDocument", () => {
168193
})
169194

170195
it("should handle mixed whitespace", () => {
171-
const doc = new MockTextDocument(" \t const x = 1")
196+
const doc = new MockTextDocument(testUri, " \t const x = 1")
172197
const line = doc.lineAt(0)
173198

174199
expect(line.text).toBe(" \t const x = 1")
@@ -177,15 +202,15 @@ describe("MockTextDocument", () => {
177202
})
178203

179204
it("should handle unicode characters", () => {
180-
const doc = new MockTextDocument("const 🚀 = 'rocket'\nconst 中文 = 'chinese'")
205+
const doc = new MockTextDocument(testUri, "const 🚀 = 'rocket'\nconst 中文 = 'chinese'")
181206

182207
expect(doc.lineCount).toBe(2)
183208
expect(doc.lineAt(0).text).toBe("const 🚀 = 'rocket'")
184209
expect(doc.lineAt(1).text).toBe("const 中文 = 'chinese'")
185210
})
186211

187212
it("should handle Windows line endings (CRLF)", () => {
188-
const doc = new MockTextDocument("line1\r\nline2\r\nline3")
213+
const doc = new MockTextDocument(testUri, "line1\r\nline2\r\nline3")
189214

190215
// Note: split("\n") will still work but will include \r in the text
191216
expect(doc.lineCount).toBe(3)
@@ -197,7 +222,7 @@ describe("MockTextDocument", () => {
197222

198223
describe("integration with vscode types", () => {
199224
it("should work with vscode.Range for getText", () => {
200-
const doc = new MockTextDocument("function test() {\n return 42\n}")
225+
const doc = new MockTextDocument(testUri, "function test() {\n return 42\n}")
201226

202227
// Create a range using vscode constructors
203228
const start = new vscode.Position(0, 9)
@@ -208,7 +233,7 @@ describe("MockTextDocument", () => {
208233
})
209234

210235
it("should return TextLine compatible with vscode interface", () => {
211-
const doc = new MockTextDocument(" const value = 'test'")
236+
const doc = new MockTextDocument(testUri, " const value = 'test'")
212237
const line = doc.lineAt(0)
213238

214239
// Verify it has all required TextLine properties

src/services/autocomplete/__tests__/MockTextDocument.ts

Lines changed: 88 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,33 @@ import * as vscode from "vscode"
33
/**
44
* A simulated vscode TextDocument for testing.
55
*/
6-
export class MockTextDocument {
6+
export class MockTextDocument implements vscode.TextDocument {
77
private contentLines: string[]
8+
public uri: vscode.Uri
9+
public fileName: string
10+
public isUntitled: boolean = false
11+
public languageId: string = "typescript"
12+
public version: number = 1
13+
public isDirty: boolean = false
14+
public isClosed: boolean = false
15+
public eol: vscode.EndOfLine = 1 // vscode.EndOfLine.LF
16+
public get encoding(): "utf8" {
17+
return "utf8"
18+
}
19+
public get notebook(): undefined {
20+
return undefined
21+
}
822

9-
constructor(content: string) {
23+
constructor(uri: vscode.Uri, content: string) {
24+
this.uri = uri
25+
this.fileName = uri.fsPath
1026
this.contentLines = content.split("\n")
1127
}
1228

1329
updateContent(newContent: string): void {
1430
this.contentLines = newContent.split("\n")
31+
this.version++
32+
this.isDirty = true
1533
}
1634

1735
getText(range?: vscode.Range): string {
@@ -49,21 +67,81 @@ export class MockTextDocument {
4967
* @param lineNumber The zero-based line number
5068
* @returns A simplified TextLine object containing the text and position information
5169
*/
52-
lineAt(lineNumber: number): vscode.TextLine {
53-
if (lineNumber < 0 || lineNumber >= this.contentLines.length) {
54-
throw new Error(`Invalid line number: ${lineNumber}`)
70+
lineAt(positionOrLine: number | vscode.Position): vscode.TextLine {
71+
const line = typeof positionOrLine === "number" ? positionOrLine : positionOrLine.line
72+
if (line < 0 || line >= this.contentLines.length) {
73+
throw new Error(`Invalid line number: ${line}`)
5574
}
5675

57-
const text = this.contentLines[lineNumber]
58-
const range = new vscode.Range(new vscode.Position(lineNumber, 0), new vscode.Position(lineNumber, text.length))
76+
const text = this.contentLines[line]
77+
const range = new vscode.Range(new vscode.Position(line, 0), new vscode.Position(line, text.length))
5978

6079
return {
6180
text,
6281
range,
63-
lineNumber,
64-
rangeIncludingLineBreak: range,
82+
lineNumber: line,
83+
rangeIncludingLineBreak: range, // Simplified for mock
6584
firstNonWhitespaceCharacterIndex: text.search(/\S|$/),
6685
isEmptyOrWhitespace: !/\S/.test(text),
67-
} as vscode.TextLine
86+
}
87+
}
88+
89+
// Add other required methods with mock implementations
90+
offsetAt(position: vscode.Position): number {
91+
let offset = 0
92+
for (let i = 0; i < position.line; i++) {
93+
if (i < this.contentLines.length) {
94+
offset += this.contentLines[i].length + 1 // +1 for newline
95+
}
96+
}
97+
offset += position.character
98+
return offset
99+
}
100+
101+
positionAt(offset: number): vscode.Position {
102+
let currentOffset = 0
103+
for (let i = 0; i < this.contentLines.length; i++) {
104+
const lineLength = this.contentLines[i].length + 1
105+
if (currentOffset + lineLength > offset) {
106+
return new vscode.Position(i, offset - currentOffset)
107+
}
108+
currentOffset += lineLength
109+
}
110+
// If offset is beyond the end of the document
111+
const lastLine = this.contentLines.length - 1
112+
const lastLineLength = this.contentLines[lastLine]?.length || 0
113+
return new vscode.Position(lastLine, lastLineLength)
114+
}
115+
116+
save(): Promise<boolean> {
117+
this.isDirty = false
118+
return Promise.resolve(true)
119+
}
120+
121+
getWordRangeAtPosition(position: vscode.Position, regex?: RegExp): vscode.Range | undefined {
122+
const line = this.lineAt(position.line)
123+
const text = line.text
124+
const wordRegex =
125+
regex || /(-?\d*\.\d\w*)|([^\`\~\!\@\#\$\%\^\&\*\(\)\-\=\+\[\{\]\}\\\|\;\:\'\"\,\.\<\>\/\?\s]+)/g
126+
127+
let match
128+
while ((match = wordRegex.exec(text)) !== null) {
129+
const start = match.index
130+
const end = start + match[0].length
131+
if (position.character >= start && position.character <= end) {
132+
return new vscode.Range(position.line, start, position.line, end)
133+
}
134+
}
135+
return undefined
136+
}
137+
138+
validateRange(range: vscode.Range): vscode.Range {
139+
// Simplified validation
140+
return range
141+
}
142+
143+
validatePosition(position: vscode.Position): vscode.Position {
144+
// Simplified validation
145+
return position
68146
}
69147
}

src/services/autocomplete/__tests__/MockTextEditor.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,18 @@ export class MockTextEditor {
2525

2626
if (cursorOffset === -1) {
2727
// No cursor marker found - default to position (0,0)
28-
this.document = new MockTextDocument(content) as unknown as vscode.TextDocument
28+
const uri = vscode.Uri.parse("untitled:mockEditor")
29+
this.document = new MockTextDocument(uri, content) as unknown as vscode.TextDocument
2930
const defaultPosition = new vscode.Position(0, 0)
3031
this.selection = new vscode.Selection(defaultPosition, defaultPosition)
3132
} else {
3233
// Cursor marker found - remove it and calculate position
3334
const cleanContent =
3435
content.substring(0, cursorOffset) + content.substring(cursorOffset + CURSOR_MARKER.length)
35-
this.document = new MockTextDocument(cleanContent) as unknown as vscode.TextDocument
36+
37+
const uri = vscode.Uri.parse("untitled:mockEditor")
38+
// Create document without cursor marker
39+
this.document = new MockTextDocument(uri, cleanContent) as unknown as vscode.TextDocument
3640

3741
// Calculate line and character for cursor position
3842
const beforeCursor = content.substring(0, cursorOffset)

0 commit comments

Comments
 (0)