Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 5 additions & 0 deletions .changeset/cli-auth-reminder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Agent Manager: remind first-time CLI installs to run `kilocode auth` after opening the install terminal, with translations.
5 changes: 5 additions & 0 deletions .changeset/ripe-bats-wish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": patch
---

Fix styling issue on task headers
1 change: 1 addition & 0 deletions apps/kilocode-docs/docs/plans/about.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ No credits are included with a Teams or Enterprise plan purchase.
- **Centralized billing** - one invoice for your whole team
- **Complete transparency** - see every request, cost, and usage pattern
- **Team management** - roles, permissions, and usage controls
- **AI Adoption Score** - see how well your team is using AI to accelerate development

**Cost:** $15 per user per month

Expand Down
8 changes: 8 additions & 0 deletions packages/telemetry/src/BaseTelemetryClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,14 @@ export abstract class BaseTelemetryClient implements TelemetryClient {
// Event properties take precedence in case of conflicts.
const mergedProperties = { ...providerProperties, ...(event.properties || {}) }

// kilocode_change start
// Add organization ID if available from provider properties
// This ensures all events include the organization ID when present
if (providerProperties.kilocodeOrganizationId && !mergedProperties.kilocodeOrganizationId) {
mergedProperties.kilocodeOrganizationId = providerProperties.kilocodeOrganizationId
}
// kilocode_change end

// Filter out properties that shouldn't be captured by this client
return Object.fromEntries(Object.entries(mergedProperties).filter(([key]) => this.isPropertyCapturable(key)))
}
Expand Down
173 changes: 173 additions & 0 deletions packages/telemetry/src/__tests__/PostHogTelemetryClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,108 @@ describe("PostHogTelemetryClient", () => {
})
})

it("should include organization ID from provider properties", async () => {
const client = new PostHogTelemetryClient()

const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockResolvedValue({
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
editorName: "vscode",
language: "en",
mode: "code",
kilocodeOrganizationId: "org-123",
}),
}

client.setProvider(mockProvider)

const getEventProperties = getPrivateProperty<
(event: { event: TelemetryEventName; properties?: Record<string, any> }) => Promise<Record<string, any>>
>(client, "getEventProperties").bind(client)

const result = await getEventProperties({
event: TelemetryEventName.TASK_CREATED,
properties: {
customProp: "value",
},
})

// Organization ID should be included
expect(result).toEqual({
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
editorName: "vscode",
language: "en",
mode: "code",
kilocodeOrganizationId: "org-123",
customProp: "value",
})
})

it("should not override organization ID from event properties", async () => {
const client = new PostHogTelemetryClient()

const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockResolvedValue({
appVersion: "1.0.0",
kilocodeOrganizationId: "org-from-provider",
}),
}

client.setProvider(mockProvider)

const getEventProperties = getPrivateProperty<
(event: { event: TelemetryEventName; properties?: Record<string, any> }) => Promise<Record<string, any>>
>(client, "getEventProperties").bind(client)

const result = await getEventProperties({
event: TelemetryEventName.TASK_CREATED,
properties: {
kilocodeOrganizationId: "org-from-event",
},
})

// Event property should take precedence
expect(result.kilocodeOrganizationId).toBe("org-from-event")
})

it("should handle missing organization ID gracefully", async () => {
const client = new PostHogTelemetryClient()

const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockResolvedValue({
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
}),
}

client.setProvider(mockProvider)

const getEventProperties = getPrivateProperty<
(event: { event: TelemetryEventName; properties?: Record<string, any> }) => Promise<Record<string, any>>
>(client, "getEventProperties").bind(client)

const result = await getEventProperties({
event: TelemetryEventName.TASK_CREATED,
properties: {
customProp: "value",
},
})

// Should not have organization ID
expect(result).not.toHaveProperty("kilocodeOrganizationId")
expect(result).toEqual({
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
customProp: "value",
})
})

it("should handle errors from provider gracefully", async () => {
const client = new PostHogTelemetryClient()

Expand Down Expand Up @@ -327,6 +429,77 @@ describe("PostHogTelemetryClient", () => {
expect(captureCall.properties).not.toHaveProperty("repositoryName")
expect(captureCall.properties).not.toHaveProperty("defaultBranch")
})

it("should include organization ID in captured events", async () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)

const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockResolvedValue({
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
editorName: "vscode",
language: "en",
mode: "code",
kilocodeOrganizationId: "org-456",
}),
}

client.setProvider(mockProvider)

await client.capture({
event: TelemetryEventName.TASK_CREATED,
properties: { test: "value" },
})

expect(mockPostHogClient.capture).toHaveBeenCalledWith({
distinctId: "test-machine-id",
event: TelemetryEventName.TASK_CREATED,
properties: expect.objectContaining({
appVersion: "1.0.0",
test: "value",
kilocodeOrganizationId: "org-456",
}),
})

// Verify organization ID is included
const captureCall = mockPostHogClient.capture.mock.calls[0][0]
expect(captureCall.properties.kilocodeOrganizationId).toBe("org-456")
})

it("should capture events without organization ID when not provided", async () => {
const client = new PostHogTelemetryClient()
client.updateTelemetryState(true)

const mockProvider: TelemetryPropertiesProvider = {
getTelemetryProperties: vi.fn().mockResolvedValue({
appVersion: "1.0.0",
vscodeVersion: "1.60.0",
platform: "darwin",
}),
}

client.setProvider(mockProvider)

await client.capture({
event: TelemetryEventName.TASK_CREATED,
properties: { test: "value" },
})

expect(mockPostHogClient.capture).toHaveBeenCalledWith({
distinctId: "test-machine-id",
event: TelemetryEventName.TASK_CREATED,
properties: expect.objectContaining({
appVersion: "1.0.0",
test: "value",
}),
})

// Verify organization ID is not included
const captureCall = mockPostHogClient.capture.mock.calls[0][0]
expect(captureCall.properties).not.toHaveProperty("kilocodeOrganizationId")
})
})

describe("updateTelemetryState", () => {
Expand Down
112 changes: 96 additions & 16 deletions src/core/kilocode/agent-manager/AgentManagerProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
parseParallelModeCompletionBranch,
} from "./parallelModeParser"
import { findKilocodeCli } from "./CliPathResolver"
import { canInstallCli, getCliInstallCommand } from "./CliInstaller"
import { CliProcessHandler, type CliProcessHandlerCallbacks } from "./CliProcessHandler"
import type { StreamEvent, KilocodeStreamEvent, KilocodePayload, WelcomeStreamEvent } from "./CliOutputParser"
import { RemoteSessionService } from "./RemoteSessionService"
Expand Down Expand Up @@ -787,32 +788,111 @@ export class AgentManagerProvider implements vscode.Disposable {
this.showCliError({ type: "spawn_error", message: "CLI not found" })
}

/**
* Open a terminal and run the CLI install command.
* Uses the terminal to ensure the user's shell environment (nvm, fnm, volta, etc.) is respected.
*/
private runInstallInTerminal(): void {
const shellPath = process.platform === "win32" ? undefined : process.env.SHELL
const shellName = shellPath ? path.basename(shellPath) : undefined
const shellArgs = process.platform === "win32" ? undefined : shellName === "zsh" ? ["-l", "-i"] : ["-l"]

const terminal = vscode.window.createTerminal({
name: "Install Kilocode CLI",
message: t("kilocode:agentManager.terminal.installMessage"),
shellPath,
shellArgs,
})
terminal.show()
terminal.sendText(getCliInstallCommand())
const authLabel = t("kilocode:agentManager.actions.loginCli")
void vscode.window
.showInformationMessage(t("kilocode:agentManager.terminal.authReminder"), authLabel)
.then((selection) => {
if (selection === authLabel) {
terminal.sendText("kilocode auth")
}
})
}

private showCliError(error?: { type: "cli_outdated" | "spawn_error" | "unknown"; message: string }): void {
let errorMessage: string
let actionLabel: string
const hasNpm = canInstallCli((msg) => this.outputChannel.appendLine(`[AgentManager] ${msg}`))

switch (error?.type) {
case "cli_outdated":
errorMessage = t("kilocode:agentManager.errors.cliOutdated")
actionLabel = t("kilocode:agentManager.actions.updateInstructions")
if (hasNpm) {
// Offer to update via terminal
const updateInTerminal = t("kilocode:agentManager.actions.runInTerminal")
const manualUpdate = t("kilocode:agentManager.actions.updateInstructions")
vscode.window
.showWarningMessage(
t("kilocode:agentManager.errors.cliOutdated"),
updateInTerminal,
manualUpdate,
)
.then((selection) => {
if (selection === updateInTerminal) {
this.runInstallInTerminal()
} else if (selection === manualUpdate) {
void vscode.env.openExternal(vscode.Uri.parse("https://kilo.ai/docs/cli"))
}
})
} else {
// No npm available, show manual instructions
const actionLabel = t("kilocode:agentManager.actions.updateInstructions")
vscode.window
.showErrorMessage(t("kilocode:agentManager.errors.cliOutdated"), actionLabel)
.then((selection) => {
if (selection === actionLabel) {
void vscode.env.openExternal(vscode.Uri.parse("https://kilo.ai/docs/cli"))
}
})
}
break
case "spawn_error":
errorMessage = t("kilocode:agentManager.errors.cliNotFound")
actionLabel = t("kilocode:agentManager.actions.installInstructions")
case "spawn_error": {
if (hasNpm) {
// Offer to install via terminal
const installInTerminal = t("kilocode:agentManager.actions.runInTerminal")
const manualInstall = t("kilocode:agentManager.actions.installInstructions")
vscode.window
.showErrorMessage(
t("kilocode:agentManager.errors.cliNotFound"),
installInTerminal,
manualInstall,
)
.then((selection) => {
if (selection === installInTerminal) {
this.runInstallInTerminal()
} else if (selection === manualInstall) {
void vscode.env.openExternal(vscode.Uri.parse("https://kilo.ai/docs/cli"))
}
})
} else {
// No npm available, show manual instructions
const actionLabel = t("kilocode:agentManager.actions.installInstructions")
vscode.window
.showErrorMessage(t("kilocode:agentManager.errors.cliNotFound"), actionLabel)
.then((selection) => {
if (selection === actionLabel) {
void vscode.env.openExternal(vscode.Uri.parse("https://kilo.ai/docs/cli"))
}
})
}
break
default:
errorMessage = error?.message
}
default: {
const errorMessage = error?.message
? t("kilocode:agentManager.errors.sessionFailedWithMessage", { message: error.message })
: t("kilocode:agentManager.errors.sessionFailed")
actionLabel = t("kilocode:agentManager.actions.getHelp")
const actionLabel = t("kilocode:agentManager.actions.getHelp")
vscode.window.showErrorMessage(errorMessage, actionLabel).then((selection) => {
if (selection === actionLabel) {
void vscode.env.openExternal(vscode.Uri.parse("https://kilo.ai/docs/cli"))
}
})
break
}

vscode.window.showErrorMessage(errorMessage, actionLabel).then((selection) => {
if (selection === actionLabel) {
void vscode.env.openExternal(vscode.Uri.parse("https://kilo.ai/docs/cli"))
}
})
}
}

/**
Expand Down
Loading