Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
bd1ba9b
fix(vscode): attribute DynamicWorkflow steps to their own subagent
elkaix Aug 5, 2026
11aaa83
feat(site): add npm downloads badge to hero
elkaix Aug 5, 2026
653612e
fix(vscode): address CodeRabbit findings on workflow lanes
elkaix Aug 5, 2026
6fe3e4b
fix(vscode): make the success-path lane test non-vacuous
elkaix Aug 5, 2026
19ced60
fix(vscode): restore neutral primary and add a brand accent token
elkaix Aug 5, 2026
c1b096d
fix(vscode): make the session permission mode survive attach and resume
elkaix Aug 5, 2026
81a7d39
fix(vscode): type the permission-mode set after the lint autofix
elkaix Aug 5, 2026
cbc5171
feat(vscode): manage model providers from the extension
elkaix Aug 5, 2026
4b10a69
fix(vscode): list the session's skills in the slash menu
elkaix Aug 5, 2026
44da7d8
fix(vscode): stop reporting a managed provider as missing its key
elkaix Aug 5, 2026
a48d656
fix(vscode): filter the slash command menu by what was typed
elkaix Aug 5, 2026
164af64
refactor: name the managed provider after the platform serving it
elkaix Aug 5, 2026
79e7eed
fix(vscode): match slash commands on the command name only
elkaix Aug 5, 2026
976acdb
fix(cli): strip the escape byte when tests compare rendered frames
elkaix Aug 5, 2026
40419aa
fix(vscode): show the logo as the first step marker of an assistant m…
elkaix Aug 5, 2026
d4fe97d
fix(vscode): align the assistant logo with the reply instead of the step
elkaix Aug 5, 2026
c298e13
fix(vscode): show the assistant logo on replies that have no steps
elkaix Aug 5, 2026
3b84425
fix(vscode): complete the slash token instead of sending on selection
elkaix Aug 5, 2026
932d798
feat(tui): animate welcome logo with blink loop and startup antenna spin
elkaix Aug 5, 2026
f6beeb7
chore(vscode): refresh the webview logo assets
elkaix Aug 5, 2026
67651e5
fix(vscode): keep the step timeline unbroken by the assistant logo
elkaix Aug 5, 2026
e9a1d20
chore: add a changeset for the vscode chat and slash command fixes
elkaix Aug 5, 2026
5902c2b
chore(vscode): align the extension version with the CLI release line
elkaix Aug 5, 2026
28335ab
feat(vscode): allow a manual publish to authenticate with Entra ID
elkaix Aug 5, 2026
04ae0d5
feat(vscode): add a one-command tag-and-publish release script
elkaix Aug 5, 2026
ce11d4b
fix(vscode): ship a decodable Marketplace icon
elkaix Aug 5, 2026
a01ec40
chore(vscode): release 0.8.3
elkaix Aug 5, 2026
e1e7bba
fix(vscode): make publishing survive a flaky registry
elkaix Aug 5, 2026
fb2690c
fix(tui): stop early frames anchoring to the shell cursor
elkaix Aug 5, 2026
de40509
chore(deps): upgrade pi-tui to 0.83.0
elkaix Aug 5, 2026
79ce0d6
fix(vscode): list skills in the slash menu before a session exists
elkaix Aug 5, 2026
725b273
chore: add changesets for the skills, TUI and publishing fixes
elkaix Aug 5, 2026
d6491e2
chore(deps): align the pi-tui range across the workspace
elkaix Aug 5, 2026
580a174
chore: refresh the flake pnpmDeps hash for the pi-tui upgrade
elkaix Aug 5, 2026
d76fa06
fix(vscode): address PR review findings
elkaix Aug 5, 2026
84e0334
fix: address second-round PR review findings
elkaix Aug 5, 2026
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
8 changes: 8 additions & 0 deletions .changeset/managed-provider-kimi-code.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"@pythoughts/pythinker-code": minor
"@pythoughts/pythinker-code-sdk": minor
---

Name the managed OAuth provider after the platform that serves it. It is reached over `auth.kimi.com` and `api.kimi.com`, but it was registered as `managed:pythinker-code`, which read as a first-party service in a client that talks to several providers. The provider id is now `managed:kimi-code`, its models are aliased `kimi-code/*`, and its credentials are stored under `oauth/kimi-code`.

This is a breaking change for an existing config: the previous entries are not rewritten, so run `pythinker login` once to provision the managed provider under its current name, then remove the stale `managed:pythinker-code` entry.
5 changes: 5 additions & 0 deletions .changeset/session-free-skill-listing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pythoughts/pythinker-code": minor
---

Resolve a workspace's skills without opening a session, so an editor panel can list them before its first message.
6 changes: 6 additions & 0 deletions .changeset/shared-catalog-provider-import.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@pythoughts/pythinker-code-sdk": minor
"@pythoughts/pythinker-code": patch
---

Add an SDK routine that imports a catalog provider and its models into the persisted config, and use it for the CLI provider import so both entry points preserve existing defaults the same way.
5 changes: 5 additions & 0 deletions .changeset/tui-render-gate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@pythoughts/pythinker-code": patch
---

Stop the fixed-layout TUI anchoring its first frames to the shell cursor, which pushed the panel border into scrollback.
5 changes: 5 additions & 0 deletions .changeset/vscode-chat-logo-and-slash-insert.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"pythinker-code": patch
---

Show the assistant logo beside replies without breaking the step timeline, complete a picked slash command in the input instead of sending it on its own, ship a decodable Marketplace icon, and retry transient registry failures when publishing.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<br />

[![npm version](https://img.shields.io/npm/v/@pythoughts/pythinker-code?style=for-the-badge&logo=npm&logoColor=white&color=CB3837&label=pythinker-code)](https://www.npmjs.com/package/@pythoughts/pythinker-code)
[![Downloads](https://img.shields.io/npm/dm/@pythoughts/pythinker-code?style=for-the-badge&logo=npm&logoColor=white&color=16a34a&label=downloads)](https://www.npmjs.com/package/@pythoughts/pythinker-code)
[![Downloads](https://img.shields.io/npm/dm/@pythoughts/pythinker-code?style=for-the-badge&logo=npm&logoColor=white&color=2b89ff&label=downloads)](https://www.npmjs.com/package/@pythoughts/pythinker-code)
[![Node.js](https://img.shields.io/badge/Node.js-26%2B-339933?style=for-the-badge&logo=nodedotjs&logoColor=white)](https://github.com/Pythoughts-labs/pythinker-code/blob/main/package.json)
[![License: MIT](https://img.shields.io/badge/License-MIT-16a34a.svg?style=for-the-badge)](https://github.com/Pythoughts-labs/pythinker-code/blob/main/LICENSE)
[![CI](https://img.shields.io/github/actions/workflow/status/Pythoughts-labs/pythinker-code/ci.yml?branch=main&label=CI&style=for-the-badge&logo=githubactions&logoColor=white)](https://github.com/Pythoughts-labs/pythinker-code/actions/workflows/ci.yml?query=branch%3Amain)
Expand Down
2 changes: 1 addition & 1 deletion apps/pythinker-code/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@
"web-tree-sitter": "0.25.10"
},
"devDependencies": {
"@earendil-works/pi-tui": "^0.81.1",
"@earendil-works/pi-tui": "^0.83.0",
"@pythoughts/acp-adapter": "workspace:^",
"@pythoughts/dashboard-server": "workspace:^",
"@pythoughts/dashboard-web": "workspace:*",
Expand Down
87 changes: 16 additions & 71 deletions apps/pythinker-code/src/cli/sub/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,16 @@ import {
CustomRegistryApiError,
fetchCustomRegistry,
type CustomRegistrySource,
type ManagedPythinkerConfigShape,
type ManagedKimiConfigShape,
} from '@pythoughts/pythinker-code-oauth';
import {
applyCatalogProvider,
catalogBaseUrl,
catalogConnectionWire,
catalogProviderModels,
CatalogFetchError,
createPythinkerHarness,
DEFAULT_CATALOG_URL,
fetchCatalog,
importCatalogProvider,
inferWireType,
type Catalog,
type CatalogProviderEntry,
Expand Down Expand Up @@ -366,74 +365,20 @@ export async function handleCatalogAdd(
}

const harness = deps.getHarness();
await harness.ensureConfigFile();

let config = await harness.getConfig();

// Capture defaults BEFORE `removeProvider`, because that call clears
// `defaultModel` when it points at one of this provider's aliases (see
// `core-impl.ts removePythinkerProvider`). Without this, re-importing an
// already-configured provider would lose the user's previously-set default
// even when `--default-model` is not supplied.
const previousDefaultProvider = config.defaultProvider;
const previousDefaultModel = config.defaultModel;
const previousDefaultThinking = config.defaultThinking;

if (config.providers[providerId] !== undefined) {
config = await harness.removeProvider(providerId);
}

const baseUrl = catalogBaseUrl(entry, wire);
// `applyCatalogProvider` always overwrites both `defaultModel` and
// `defaultThinking`. The values we pass here are temporary; we restore
// a consistent state in the post-apply block below.
applyCatalogProvider(config, {
providerId,
catalogUrl: url,
wire,
baseUrl,
apiKey: useEnvVar ? undefined : literalApiKey,
apiKeyEnvVar: useEnvVar ? apiKeyEnvVar : undefined,
models,
selectedModelId: opts.defaultModel ?? '',
thinking: false,
});

// Resolve the final `defaultModel`:
// - If the caller asked for one, `applyCatalogProvider` already set it.
// - Else, restore the previous default ONLY when its alias still resolves
// after the catalog refresh; the catalog may have dropped the old
// model, in which case restoring would point default_model at a
// non-existent alias and break the next session.
if (opts.defaultModel === undefined) {
const stillResolves =
previousDefaultModel !== undefined &&
config.models?.[previousDefaultModel] !== undefined;
config.defaultModel = stillResolves ? previousDefaultModel : undefined;
try {
await importCatalogProvider(harness, {
providerId,
entry,
catalogUrl: url,
apiKey: useEnvVar ? undefined : literalApiKey,
apiKeyEnvVar: useEnvVar ? apiKeyEnvVar : undefined,
defaultModel: opts.defaultModel,
});
} catch (error) {
deps.stderr.write(`${errorMessage(error)}\n`);
deps.exit(1);
}

// Mirror the defaultModel handling for defaultProvider: `removeProvider`
// cleared it, and the catalog refresh may have dropped the provider id.
config.defaultProvider =
previousDefaultProvider !== undefined && config.providers[previousDefaultProvider] !== undefined
? previousDefaultProvider
: undefined;

// Always restore `defaultThinking` from what was there before — including
// `undefined`. Persisting `false` when the user never set it would make
// `resolveThinkingLevel` (agent-core/src/agent/config/thinking.ts) treat
// it as an explicit "off" request and silently disable thinking, even
// for thinking-capable models.
config.defaultThinking = previousDefaultThinking;

await harness.setConfig({
providers: config.providers,
models: config.models,
defaultProvider: config.defaultProvider,
defaultModel: config.defaultModel,
defaultThinking: config.defaultThinking,
});

const displayName = entry.name ?? providerId;
deps.stdout.write(
`Imported ${displayName} (${providerId}) with ${String(models.length)} model${models.length === 1 ? '' : 's'} from ${url}.\n`,
Expand Down Expand Up @@ -572,8 +517,8 @@ function resolveApiKey(flag: string | undefined, env: NodeJS.ProcessEnv): string
return undefined;
}

function asManaged(config: PythinkerConfig): ManagedPythinkerConfigShape {
return config as unknown as ManagedPythinkerConfigShape;
function asManaged(config: PythinkerConfig): ManagedKimiConfigShape {
return config as unknown as ManagedKimiConfigShape;
}

function providerSourceLabel(provider: PythinkerConfig['providers'][string]): string {
Expand Down
6 changes: 3 additions & 3 deletions apps/pythinker-code/src/cli/telemetry.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createPythinkerDeviceId, PYTHINKER_CODE_PROVIDER_NAME } from '@pythoughts/pythinker-code-oauth';
import { createPythinkerDeviceId, KIMI_CODE_PROVIDER_NAME } from '@pythoughts/pythinker-code-oauth';
import {
PythinkerAuthFacade,
loadRuntimeConfigSafe,
Expand Down Expand Up @@ -55,7 +55,7 @@ export function initializeCliTelemetry(options: InitializeCliTelemetryOptions):
uiMode: options.uiMode,
model: options.model ?? options.config.defaultModel,
getAccessToken: async () =>
(await options.harness.auth.getCachedAccessToken(PYTHINKER_CODE_PROVIDER_NAME)) ?? null,
(await options.harness.auth.getCachedAccessToken(KIMI_CODE_PROVIDER_NAME)) ?? null,
});
if (options.bootstrap.firstLaunch) {
options.harness.track('first_launch');
Expand Down Expand Up @@ -102,7 +102,7 @@ export function initializeServerTelemetry(
version: options.version,
uiMode: WEB_UI_MODE,
model: config.defaultModel,
getAccessToken: async () => (await auth.getCachedAccessToken(PYTHINKER_CODE_PROVIDER_NAME)) ?? null,
getAccessToken: async () => (await auth.getCachedAccessToken(KIMI_CODE_PROVIDER_NAME)) ?? null,
});

return {
Expand Down
2 changes: 1 addition & 1 deletion apps/pythinker-code/src/constant/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const PYTHINKER_CODE_BANNER_DIR_NAME = 'banner';
export const PYTHINKER_CODE_BANNER_STATE_FILE_NAME = 'state.json';

// Managed Pythinker auth provider key shared with OAuth/SDK config.
export const DEFAULT_OAUTH_PROVIDER_NAME = 'managed:pythinker-code';
export { KIMI_CODE_PROVIDER_NAME as DEFAULT_OAUTH_PROVIDER_NAME } from '@pythoughts/pythinker-code-oauth';

// SDK/core error code that tells the TUI to show a login-required startup
// notice. Derived from sdk's ErrorCodes so a future rename in core
Expand Down
14 changes: 7 additions & 7 deletions apps/pythinker-code/src/tui/commands/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import {
OpenAICodexApiError,
OpenPlatformApiError,
runOpenAICodexOAuthFlow,
type ManagedPythinkerCodeModelInfo,
type ManagedPythinkerConfigShape,
type ManagedKimiCodeModelInfo,
type ManagedKimiConfigShape,
type OpenPlatformDefinition,
} from '@pythoughts/pythinker-code-oauth';
import {
Expand Down Expand Up @@ -54,7 +54,7 @@ export async function handleLoginCommand(host: SlashCommandHost): Promise<void>
return;
}

if (platformId === 'pythinker-code') {
if (platformId === 'kimi-code') {
await handlePythinkerCodeOAuthLogin(host);
return;
}
Expand Down Expand Up @@ -156,7 +156,7 @@ async function handleOpenPlatformLogin(
};
host.cancelInFlight = cancelLogin;

let models: ManagedPythinkerCodeModelInfo[];
let models: ManagedKimiCodeModelInfo[];
try {
models = await fetchOpenPlatformModels(platform, apiKey, fetch, controller.signal);
models = filterModelsByPrefix(models, platform);
Expand Down Expand Up @@ -193,7 +193,7 @@ async function handleOpenPlatformLogin(
}

const config = await host.harness.getConfig();
applyOpenPlatformConfig(config as ManagedPythinkerConfigShape, {
applyOpenPlatformConfig(config as ManagedKimiConfigShape, {
platform,
models,
selectedModel: selection.model,
Expand Down Expand Up @@ -358,7 +358,7 @@ async function handleOpenAICodexOAuthLogin(host: SlashCommandHost): Promise<void
await host.harness.removeProvider(OPENAI_CODEX_PROVIDER_ID);
}

let models: ManagedPythinkerCodeModelInfo[];
let models: ManagedKimiCodeModelInfo[];
try {
models = await fetchOpenAICodexModels({
accessToken: tokens.accessToken,
Expand Down Expand Up @@ -388,7 +388,7 @@ async function handleOpenAICodexOAuthLogin(host: SlashCommandHost): Promise<void
if (selection === undefined) return;

const config = await host.harness.getConfig();
applyOpenAICodexOAuthConfig(config as ManagedPythinkerConfigShape, {
applyOpenAICodexOAuthConfig(config as ManagedKimiConfigShape, {
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken,
accountId: tokens.accountId,
Expand Down
6 changes: 3 additions & 3 deletions apps/pythinker-code/src/tui/commands/prompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from '@pythoughts/pythinker-code-sdk';
import { capabilitiesForModel } from '@pythoughts/pythinker-code-oauth';
import type {
ManagedPythinkerCodeModelInfo,
ManagedKimiCodeModelInfo,
OpenPlatformDefinition,
} from '@pythoughts/pythinker-code-oauth';

Expand Down Expand Up @@ -159,9 +159,9 @@ export function promptCatalogProviderSelection(host: SlashCommandHost, catalog:

export async function promptModelSelectionForOpenPlatform(
host: SlashCommandHost,
models: ManagedPythinkerCodeModelInfo[],
models: ManagedKimiCodeModelInfo[],
platform: OpenPlatformDefinition,
): Promise<{ model: ManagedPythinkerCodeModelInfo; effort: string } | undefined> {
): Promise<{ model: ManagedKimiCodeModelInfo; effort: string } | undefined> {
const modelDict: Record<string, ModelAlias> = {};
for (const m of models) {
modelDict[`${platform.id}/${m.id}`] = {
Expand Down
4 changes: 2 additions & 2 deletions apps/pythinker-code/src/tui/commands/provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {
applyCustomRegistryEntries,
fetchCustomRegistry,
type CustomRegistrySource,
type ManagedPythinkerConfigShape,
type ManagedKimiConfigShape,
} from '@pythoughts/pythinker-code-oauth';
import {
CatalogFetchError,
Expand Down Expand Up @@ -212,7 +212,7 @@ async function handleCustomRegistryAddViaDialog(host: SlashCommandHost): Promise
try {
const config = await host.harness.getConfig();
applyCustomRegistryEntries(
config as unknown as ManagedPythinkerConfigShape,
config as unknown as ManagedKimiConfigShape,
entries,
source,
);
Expand Down
54 changes: 7 additions & 47 deletions apps/pythinker-code/src/tui/commands/skills.ts
Original file line number Diff line number Diff line change
@@ -1,56 +1,16 @@
import type { Session, SkillSummary } from '@pythoughts/pythinker-code-sdk';
import {
buildSkillSlashCommands,
isUserActivatableSkill,
type Session,
} from '@pythoughts/pythinker-code-sdk';

import { NO_ACTIVE_SESSION_MESSAGE } from '../constant/pythinker-tui';
import type { SlashCommandHost } from './dispatch';
import type { PythinkerSlashCommand } from './types';

export type SkillListSession = Pick<Session, 'listSkills'>;

export interface SkillSlashCommands {
readonly commands: readonly PythinkerSlashCommand[];
readonly commandMap: ReadonlyMap<string, string>;
}

export function isUserActivatableSkill(skill: SkillSummary): boolean {
return (
skill.userInvocable !== false &&
(skill.type === undefined ||
skill.type === 'prompt' ||
skill.type === 'inline' ||
skill.type === 'flow')
);
}

function compareSkillSlashCommands(a: SkillSummary, b: SkillSummary): number {
return (
getSkillSlashCommandGroup(a.source) - getSkillSlashCommandGroup(b.source) ||
a.name.localeCompare(b.name)
);
}

function getSkillSlashCommandGroup(source: SkillSummary['source']): number {
return source === 'builtin' ? 0 : 1;
}

export function buildSkillSlashCommands(skills: readonly SkillSummary[]): SkillSlashCommands {
const commandMap = new Map<string, string>();
const sortedSkills = [...skills].toSorted(compareSkillSlashCommands);
const commands = sortedSkills.filter(isUserActivatableSkill).map((skill) => {
const commandName =
skill.commandName ??
(skill.source === 'builtin' || skill.isSubSkill === true
? skill.name
: `skill:${skill.name}`);
commandMap.set(commandName, skill.name);
return {
name: commandName,
aliases: [],
description: skill.description ?? '',
argumentHint: skill.argumentHint,
};
});
return { commands, commandMap };
}
export type { SkillSlashCommands } from '@pythoughts/pythinker-code-sdk';
export { buildSkillSlashCommands, isUserActivatableSkill };

export async function handleSkillsCommand(
host: SlashCommandHost,
Expand Down
Loading
Loading