Skip to content

Commit a3d4846

Browse files
markijbemakilo-code-bot[bot]lambertjosh
authored
feat(notifications): add upgrade nudge for legacy extension users (version < 7) (#4316)
* feat(notifications): add version-filtered upgrade nudge for legacy extension users Add extensionVersionBelow field to KiloNotification to allow targeting users by extension major version. Update the notifications API route to extract the client version from X-KiloCode-Version / User-Agent headers and pass it into generateUserNotifications for server-side filtering. Add a new notification that shows only to kilocode-legacy users (version < 7), directing them to upgrade to the latest Kilo Code extension. Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> * fix(notifications): use type import for NextRequest to satisfy lint Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> * fix(notifications): update legacy EOL notification with accurate messaging and correct URL Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> * fix(notifications): treat missing extension version as legacy and parse opencode UA The legacy extension's notification fetch sends only Authorization + Content-Type, so it has no version header. The prior logic hid version-gated notifications when the version was unknown and only parsed X-KiloCode-Version / Kilo-Code UA, neither of which the current extension sends. Result: legacy users (the target) never saw the EOL nudge, and the current extension (opencode-kilo-provider/<version>) also resolved to undefined. Now parse the opencode-kilo-provider User-Agent so >=7 clients are correctly excluded, and treat an unknown version as an old/pre-versioning client that IS shown the notification. * refactor(notifications): target legacy EOL notice by axios User-Agent The legacy extension calls the notifications endpoint with axios, sending User-Agent: axios/<version>, while the current extension/CLI use the shared Kilo gateway headers (opencode-kilo-provider/...). Detecting the axios UA is a precise, positive signal for the legacy extension, replacing the version-gating heuristic that treated absent version data as old. * docs(notifications): reword legacy EOL notice for 5.x and upgrade-in-place Both the old and new extension are the same Kilo Code marketplace extension, so reference the 5.x versions and tell users to upgrade to the latest version rather than switch to a different extension. * Update apps/web/src/lib/notifications.ts Co-authored-by: Joshua Lambert <25085430+lambertjosh@users.noreply.github.com> * refactor(notifications): scope legacy UA helper name to notifications Address chrarnoldus review: an axios User-Agent is not a general 'legacy extension' signal (axios is a generic HTTP client also used for LLM calls). Rename isLegacyKiloExtensionUserAgent -> isLegacyKiloExtensionNotificationsUserAgent to make clear the heuristic is only meaningful for the notifications endpoint. --------- Co-authored-by: markijbema <624143+markijbema@users.noreply.github.com> Co-authored-by: kiloconnect[bot] <240665456+kiloconnect[bot]@users.noreply.github.com> Co-authored-by: Joshua Lambert <25085430+lambertjosh@users.noreply.github.com>
1 parent a1be77f commit a3d4846

5 files changed

Lines changed: 99 additions & 10 deletions

File tree

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
11
import type { KiloNotification } from '@/lib/notifications';
22
import { generateUserNotifications } from '@/lib/notifications';
33
import { getUserFromAuth } from '@/lib/user/server';
4-
import { NextResponse } from 'next/server';
4+
import { isLegacyKiloExtensionNotificationsUserAgent } from '@/lib/userAgent';
5+
import { type NextRequest, NextResponse } from 'next/server';
56

6-
export async function GET(): Promise<
7-
NextResponse<{ error: string } | { notifications: KiloNotification[] }>
8-
> {
7+
export async function GET(
8+
request: NextRequest
9+
): Promise<NextResponse<{ error: string } | { notifications: KiloNotification[] }>> {
910
const { user, authFailedResponse } = await getUserFromAuth({
1011
adminOnly: false,
1112
});
1213

1314
if (authFailedResponse) return authFailedResponse;
1415

15-
const notifications = await generateUserNotifications(user);
16+
// The legacy extension calls this endpoint with axios (User-Agent: axios/<version>),
17+
// while the current extension/CLI use the shared Kilo gateway headers. Detecting the
18+
// axios User-Agent lets us target the legacy-extension end-of-life notice precisely.
19+
const isLegacyExtension = isLegacyKiloExtensionNotificationsUserAgent(
20+
request.headers.get('user-agent')
21+
);
22+
23+
const notifications = await generateUserNotifications(user, { isLegacyExtension });
1624

1725
return NextResponse.json({ notifications });
1826
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { describe, test, expect } from '@jest/globals';
2+
import { passesLegacyExtensionGate } from './notifications';
3+
4+
describe('passesLegacyExtensionGate', () => {
5+
test('always shows notifications not gated to the legacy extension', () => {
6+
expect(passesLegacyExtensionGate({}, false)).toBe(true);
7+
expect(passesLegacyExtensionGate({}, true)).toBe(true);
8+
expect(passesLegacyExtensionGate({ showOnlyOnLegacyExtension: false }, false)).toBe(true);
9+
});
10+
11+
test('shows legacy-gated notifications only to the legacy extension', () => {
12+
expect(passesLegacyExtensionGate({ showOnlyOnLegacyExtension: true }, true)).toBe(true);
13+
expect(passesLegacyExtensionGate({ showOnlyOnLegacyExtension: true }, false)).toBe(false);
14+
});
15+
});

‎apps/web/src/lib/notifications.ts‎

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,39 @@ export type KiloNotification = {
3333
showIn?: ('extension' | 'extension-native' | 'cli')[];
3434
// ISO 8601 timestamp after which this notification should no longer be shown
3535
expiresAt?: string;
36+
// When true, only show to the legacy ("Roo-based") Kilo Code extension, identified by
37+
// its axios User-Agent. Used to target end-of-life notices at legacy-extension users.
38+
showOnlyOnLegacyExtension?: boolean;
3639
};
3740

41+
/**
42+
* Decide whether a legacy-targeted notification should be shown to a client.
43+
* Notifications flagged showOnlyOnLegacyExtension are shown only when the request
44+
* came from the legacy extension (detected via its axios User-Agent).
45+
*/
46+
export function passesLegacyExtensionGate(
47+
notification: Pick<KiloNotification, 'showOnlyOnLegacyExtension'>,
48+
isLegacyExtension: boolean
49+
): boolean {
50+
if (!notification.showOnlyOnLegacyExtension) return true;
51+
return isLegacyExtension;
52+
}
53+
3854
const normalUnconditionalNotifications: KiloNotification[] = [
3955
//If you need to check or personalize the notification, see examples at the bottom of this file
4056
//if you just want a simple straightforward global message, add it here.
57+
{
58+
id: 'legacy-upgrade-june-2026',
59+
title: 'Kilo Code 5.x: End of Life July 31, 2026',
60+
message:
61+
'Kilo Code extension version 5.x reaches end of life on July 31, 2026. After that date, it will no longer receive updates, bug fixes, or security patches. Upgrade to the latest version for continued support.',
62+
action: {
63+
actionText: 'See End of Life Notice',
64+
actionURL: 'https://github.com/Kilo-Org/kilocode-legacy#legacy-ide-extensions-end-of-life',
65+
},
66+
showIn: ['extension'],
67+
showOnlyOnLegacyExtension: true,
68+
},
4169
{
4270
id: 'star-giveaway-june-2026',
4371
title: 'GitHub Star Giveaway',
@@ -113,7 +141,10 @@ const normalUnconditionalNotifications: KiloNotification[] = [
113141
},
114142
];
115143

116-
export async function generateUserNotifications(user: User): Promise<KiloNotification[]> {
144+
export async function generateUserNotifications(
145+
user: User,
146+
{ isLegacyExtension = false }: { isLegacyExtension?: boolean } = {}
147+
): Promise<KiloNotification[]> {
117148
// Pre-fetch shared data once to avoid duplicate DB queries across generators.
118149
// This eliminates ~5 redundant queries per request (2× userHasOrganizations,
119150
// 3× getUserOrganizationsWithSeats, 2× getBalanceForUser → 1+1 queries).
@@ -147,9 +178,11 @@ export async function generateUserNotifications(user: User): Promise<KiloNotific
147178
).flat();
148179

149180
const now = new Date();
150-
return [...resolvedConditionalNotifications, ...normalUnconditionalNotifications].filter(
151-
n => !n.expiresAt || new Date(n.expiresAt) > now
152-
);
181+
return [...resolvedConditionalNotifications, ...normalUnconditionalNotifications].filter(n => {
182+
if (n.expiresAt && new Date(n.expiresAt) <= now) return false;
183+
if (!passesLegacyExtensionGate(n, isLegacyExtension)) return false;
184+
return true;
185+
});
153186
}
154187

155188
async function generateLowCreditNotification(

‎apps/web/src/lib/userAgent.test.ts‎

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import { describe, test, expect } from '@jest/globals';
2-
import { getKiloCodeVersionNumber, getXKiloCodeVersionNumber } from './userAgent';
2+
import {
3+
getKiloCodeVersionNumber,
4+
getXKiloCodeVersionNumber,
5+
isLegacyKiloExtensionNotificationsUserAgent,
6+
} from './userAgent';
37

48
describe('getKiloCodeVersionNumber', () => {
59
test('returns undefined for non-Kilo-Code user agents', () => {
@@ -51,3 +55,21 @@ describe('getKiloCodeVersionNumber', () => {
5155
expect(getXKiloCodeVersionNumber('4.65.3-alpha.1 extra-info')).toBeCloseTo(4.065003, 10);
5256
});
5357
});
58+
59+
describe('isLegacyKiloExtensionNotificationsUserAgent', () => {
60+
test('matches the legacy extension axios User-Agent', () => {
61+
expect(isLegacyKiloExtensionNotificationsUserAgent('axios/1.7.2')).toBe(true);
62+
expect(isLegacyKiloExtensionNotificationsUserAgent('axios/0.27.2')).toBe(true);
63+
});
64+
65+
test('does not match current extension, CLI, bot, or missing User-Agents', () => {
66+
expect(isLegacyKiloExtensionNotificationsUserAgent(null)).toBe(false);
67+
expect(isLegacyKiloExtensionNotificationsUserAgent(undefined)).toBe(false);
68+
expect(isLegacyKiloExtensionNotificationsUserAgent('')).toBe(false);
69+
expect(isLegacyKiloExtensionNotificationsUserAgent('opencode-kilo-provider/7.1.0')).toBe(false);
70+
expect(isLegacyKiloExtensionNotificationsUserAgent('opencode-kilo-provider')).toBe(false);
71+
expect(isLegacyKiloExtensionNotificationsUserAgent('Kilo-Code/5.1.0')).toBe(false);
72+
expect(isLegacyKiloExtensionNotificationsUserAgent('Mozilla/5.0 Test Browser')).toBe(false);
73+
expect(isLegacyKiloExtensionNotificationsUserAgent('my-axios/1.0.0')).toBe(false);
74+
});
75+
});

‎apps/web/src/lib/userAgent.ts‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,17 @@ export function getKiloCodeVersionNumber(userAgent: string | null | undefined):
33
if (!userAgent || !userAgent.startsWith(userAgentPrefix)) return undefined;
44
return getXKiloCodeVersionNumber(userAgent.slice(userAgentPrefix.length));
55
}
6+
7+
// The legacy ("Roo-based") Kilo Code extension fetches notifications with axios from
8+
// the Node extension host, which sends `User-Agent: axios/<version>`. The current
9+
// extension and CLI use the shared Kilo gateway headers instead (`opencode-kilo-provider/...`).
10+
// This heuristic is only meaningful for the notifications endpoint: axios is a generic
11+
// HTTP client (also used for LLM calls), so it is not a general "is legacy extension" signal.
12+
export function isLegacyKiloExtensionNotificationsUserAgent(
13+
userAgent: string | null | undefined
14+
): boolean {
15+
return !!userAgent && userAgent.startsWith('axios/');
16+
}
617
export function getXKiloCodeVersionNumber(
718
userAgent: string | null | undefined
819
): number | undefined {

0 commit comments

Comments
 (0)