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/vscode-chat-fixes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"pythinker-code": patch
---

Render long conversations with a virtualized list, fix control overlap at narrow sidebar widths, and make sign-out work from the command palette.
5 changes: 5 additions & 0 deletions .changeset/vscode-composer-menu.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"pythinker-code": minor
---

Combine permission mode, plan mode, and thinking effort into one composer menu, and answer approval prompts with number keys.
5 changes: 5 additions & 0 deletions .changeset/vscode-config-hub.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"pythinker-code": minor
---

Add a config hub page that shows models, providers, MCP servers, the local config file, and extension settings in one place.
5 changes: 5 additions & 0 deletions .changeset/vscode-editor-theme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"pythinker-code": minor
---

The extension UI now follows the editor color theme, including light, dark, and high-contrast themes.
5 changes: 5 additions & 0 deletions .changeset/vscode-host-integrations.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"pythinker-code": minor
---

Add a status bar indicator, quick-fix code actions, terminal and editor context menu entries, a getting-started walkthrough, and a new-conversation keybinding.
83 changes: 77 additions & 6 deletions apps/vscode/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@
"title": "Pythinker Code: Insert Current File",
"icon": "$(mention)"
},
{
"command": "pythinker.addTerminalSelection",
"title": "Pythinker Code: Add Terminal Selection to Pythinker",
"icon": "$(terminal)"
},
{
"command": "pythinker.newConversation",
"title": "Pythinker Code: New Conversation",
Expand Down Expand Up @@ -154,6 +159,64 @@
"key": "alt+k",
"mac": "alt+k",
"when": "editorTextFocus"
},
{
"command": "pythinker.newConversation",
"key": "ctrl+alt+n",
"mac": "cmd+alt+n"
}
],
"walkthroughs": [
{
"id": "pythinkerGettingStarted",
"title": "Get started with Pythinker Code",
"description": "Set up Pythinker Code and run your first conversation.",
"steps": [
{
"id": "openView",
"title": "Open the Pythinker view",
"description": "Select the Pythinker icon in the Activity Bar to open the chat view.\n[Open Pythinker](command:pythinker.webview.focus)",
"media": {
"markdown": "walkthrough/open-view.md"
},
"completionEvents": [
"onView:pythinker.webview"
]
},
{
"id": "signIn",
"title": "Sign in or add a provider",
"description": "Sign in with your Pythinker account, or add a model provider with your own API key.",
"media": {
"markdown": "walkthrough/sign-in.md"
},
"completionEvents": [
"onContext:pythinker.isLoggedIn"
]
},
{
"id": "firstConversation",
"title": "Run your first conversation",
"description": "Send a request and learn how the approval modes control what the agent can do.\n[New Conversation](command:pythinker.newConversation)",
"media": {
"markdown": "walkthrough/first-conversation.md"
},
"completionEvents": [
"onCommand:pythinker.newConversation"
]
},
{
"id": "referenceCode",
"title": "Reference your code",
"description": "Use @ mentions and Alt+K to point Pythinker at the exact files and lines you mean.",
"media": {
"markdown": "walkthrough/reference-code.md"
},
"completionEvents": [
"onCommand:pythinker.insertMention"
]
}
]
}
],
"viewsContainers": {
Expand Down Expand Up @@ -208,6 +271,10 @@
"command": "pythinker.insertMention",
"when": "true"
},
{
"command": "pythinker.addTerminalSelection",
"when": "false"
},
{
"command": "pythinker.newConversation",
"when": "true"
Expand All @@ -228,8 +295,14 @@
"editor/context": [
{
"command": "pythinker.insertMention",
"group": "pythoughts",
"when": "editorTextFocus"
"group": "pythinker@1",
"when": "editorHasSelection"
}
],
"terminal/context": [
{
"command": "pythinker.addTerminalSelection",
"group": "pythinker@1"
}
]
}
Expand Down Expand Up @@ -259,12 +332,11 @@
"@types/node": "^22.15.3",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@types/react-scroll-to-bottom": "^4.2.5",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/vscode": "1.100.0",
"@vitejs/plugin-react": "^4.4.1",
"@vscode/test-cli": "^0.0.11",
"@vscode/test-electron": "^2.5.2",
"@vscode/test-electron": "^3.1.0",
"@vscode/vsce": "3.9.2",
"acorn": "8.17.0",
"ovsx": "1.0.2",
Expand All @@ -275,7 +347,6 @@
},
"dependencies": {
"@base-ui/react": "^1.0.0",
"@fontsource-variable/inter": "5.2.8",
"@pythoughts/pythinker-code-sdk": "workspace:^",
"@radix-ui/react-accordion": "^1.2.12",
"@tabler/icons-react": "^3.36.0",
Expand All @@ -293,8 +364,8 @@
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-markdown": "^10.1.0",
"react-scroll-to-bottom": "^4.2.0",
"react-syntax-highlighter": "^16.1.0",
"react-virtuoso": "^4.18.11",
"rehype-katex": "^7.0.1",
"remark-gfm": "^4.0.1",
"remark-math": "^6.0.0",
Expand Down
3 changes: 2 additions & 1 deletion apps/vscode/shared/bridge.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export const Methods = {
Login: "login",
Logout: "logout",
SaveConfig: "saveConfig",
GetConfigInfo: "getConfigInfo",
GetExtensionConfig: "getExtensionConfig",
SaveExtensionConfig: "saveExtensionConfig",
OpenSettings: "openSettings",
Expand Down Expand Up @@ -103,7 +104,6 @@ export const Events = {
InsertMention: "insertMention",
NewConversation: "newConversation",
FileChangesUpdated: "fileChangesUpdated",
RollbackInput: "rollbackInput",
} as const;

const rpcMethods = new Set<string>(Object.values(Methods));
Expand Down Expand Up @@ -142,6 +142,7 @@ function validateParams(method: RpcMethod, params: unknown): boolean {
case Methods.CheckLoginStatus:
case Methods.Login:
case Methods.Logout:
case Methods.GetConfigInfo:
case Methods.GetExtensionConfig:
case Methods.OpenSettings:
case Methods.OpenFolder:
Expand Down
2 changes: 2 additions & 0 deletions apps/vscode/shared/legacy-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,8 @@ export interface ModelConfig {
name: string;
provider: string;
capabilities: string[];
/** Maximum context size in tokens, from the model alias config (maxContextSize). */
contextWindow?: number;
adaptive_thinking?: boolean;
support_efforts?: string[];
default_effort?: string;
Expand Down
7 changes: 7 additions & 0 deletions apps/vscode/shared/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ export interface ExtensionConfig {
version: string;
}

/** The user's local config file, verbatim. Content is the raw on-disk text and may contain API keys. */
export interface ConfigInfo {
path: string | null;
exists: boolean;
content: string | null;
}

export interface WorkspaceStatus {
hasWorkspace: boolean;
path?: string;
Expand Down
26 changes: 26 additions & 0 deletions apps/vscode/src/PythinkerWebviewProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ function getNonce(): string {
export class PythinkerWebviewProvider implements vscode.WebviewViewProvider {
private webviews = new Map<string, vscode.Webview>();
private bridgeHandler: BridgeHandler;
private sidebarView: vscode.WebviewView | undefined;
private badgeCount = 0;

constructor(
private readonly extensionUri: vscode.Uri,
Expand Down Expand Up @@ -51,13 +53,37 @@ export class PythinkerWebviewProvider implements vscode.WebviewViewProvider {
resolveWebviewView(webviewView: vscode.WebviewView): void {
const webviewId = `sidebar_${crypto.randomUUID()}`;
this.setupWebview(webviewId, webviewView.webview);
this.sidebarView = webviewView;
this.applyBadge();

webviewView.onDidDispose(() => {
void this.bridgeHandler.disposeView(webviewId);
this.webviews.delete(webviewId);
if (this.sidebarView === webviewView) this.sidebarView = undefined;
});
}

/** Shows `count` pending approval requests on the sidebar view; 0 clears the badge. */
setSidebarBadge(count: number): void {
this.badgeCount = count;
this.applyBadge();
}

private applyBadge(): void {
const view = this.sidebarView;
if (view === undefined) return;
view.badge =
this.badgeCount > 0
? {
value: this.badgeCount,
tooltip:
this.badgeCount === 1
? "1 approval request waits for your decision"
: `${this.badgeCount} approval requests wait for your decision`,
}
: undefined;
}

createPanel(): vscode.WebviewPanel {
const webviewId = `panel_${crypto.randomUUID()}`;

Expand Down
42 changes: 42 additions & 0 deletions apps/vscode/src/activity.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* Host-side activity counters for the status bar and view badge. Streams are
* counted around the StreamChat handler; approvals and questions are counted
* in the reverse-RPC controller. No `vscode` import on purpose: the runtime
* files that call in here run under plain-node vitest.
*/

export interface ActivityStatus {
readonly activeStreams: number;
readonly pendingApprovals: number;
}

type ActivityListener = (status: ActivityStatus) => void;

let activeStreams = 0;
let pendingApprovals = 0;
const listeners = new Set<ActivityListener>();

export function getActivity(): ActivityStatus {
return { activeStreams, pendingApprovals };
}

export function onActivityChange(listener: ActivityListener): () => void {
listeners.add(listener);
return () => listeners.delete(listener);
}

export function trackStream(delta: number): void {
activeStreams = Math.max(0, activeStreams + delta);
fire();
}

export function trackApprovals(delta: number): void {
if (delta === 0) return;
pendingApprovals = Math.max(0, pendingApprovals + delta);
fire();
}

function fire(): void {
const status = getActivity();
for (const listener of listeners) listener(status);
}
2 changes: 1 addition & 1 deletion apps/vscode/src/config/vscode-settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const VSCodeSettings = {
},

get showThinkingContent(): boolean {
return getConfig().get<boolean>("showThinkingContent", false);
return getConfig().get<boolean>("showThinkingContent", true);
},

get showThinkingExpanded(): boolean {
Expand Down
56 changes: 51 additions & 5 deletions apps/vscode/src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import * as vscode from "vscode";

import { Events } from "../shared/bridge";
import { PythinkerWebviewProvider } from "./PythinkerWebviewProvider";
import { getActivity, onActivityChange, type ActivityStatus } from "./activity";
import { onSettingsChange, VSCodeSettings } from "./config/vscode-settings";
import { performLogout } from "./handlers/auth.handler";
import { clearInputHistory } from "./handlers/workspace.handler";
import { registerChatContext } from "./integrations/chat-context";
import { defaultPermissionMode } from "./runtime/permission-mode";
import { updateLoginContext } from "./utils/context";

Expand Down Expand Up @@ -56,11 +60,35 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
webviewOptions: { retainContextWhenHidden: true },
}));

const statusItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, 100);
statusItem.name = "Pythinker Code";
statusItem.command = "pythinker.webview.focus";
const renderActivity = (activity: ActivityStatus): void => {
if (activity.pendingApprovals > 0) {
statusItem.text = "$(bell) Pythinker: approval needed";
statusItem.tooltip =
activity.pendingApprovals === 1
? "1 request waits for your decision"
: `${activity.pendingApprovals} requests wait for your decision`;
statusItem.backgroundColor = new vscode.ThemeColor("statusBarItem.warningBackground");
} else if (activity.activeStreams > 0) {
statusItem.text = "$(sync~spin) Pythinker";
statusItem.tooltip = "Pythinker works on your request";
statusItem.backgroundColor = undefined;
} else {
statusItem.text = "$(sparkle) Pythinker";
statusItem.tooltip = "Open Pythinker Code";
statusItem.backgroundColor = undefined;
}
statusItem.show();
provider?.setSidebarBadge(activity.pendingApprovals);
};
renderActivity(getActivity());
context.subscriptions.push(statusItem, { dispose: onActivityChange(renderActivity) });

const commands: Record<string, () => void | Promise<void>> = {
"pythinker.clearAllState": async () => {
await context.globalState.update("pythinker.config", undefined);
await context.globalState.update("pythinker.mcpServers", undefined);
await context.workspaceState.update("pythinker.mcpEnabled", undefined);
await clearInputHistory(context.workspaceState);
await vscode.window.showInformationMessage("Pythinker: Extension UI state cleared.");
},
"pythinker.openInTab": () => {
Expand Down Expand Up @@ -91,15 +119,33 @@ export async function activate(context: vscode.ExtensionContext): Promise<void>
"pythinker.showLogs": () => outputChannel?.show(),
"pythinker.resetPythinker": () => provider?.resetAllWebviews(),
"pythinker.logout": async () => {
await vscode.commands.executeCommand("pythinker.webview.focus");
await vscode.window.showInformationMessage("Use the logout button in Pythinker settings.");
if (!provider) return;
const result = await performLogout(provider.harness, logError);
if (!result.success) {
await vscode.window.showErrorMessage(
`Pythinker: Sign out failed.${result.error ? ` ${result.error}` : ""}`,
);
return;
}
// No login-state broadcast exists on the bridge; a reload re-runs the
// webview init, which re-checks the login status and provider list.
provider.reloadAllWebviews();
},
};

for (const [id, handler] of Object.entries(commands)) {
context.subscriptions.push(vscode.commands.registerCommand(id, handler));
}

context.subscriptions.push(
...registerChatContext({
insertMention: (uri, selection) =>
provider?.insertEditorMention(uri, selection) ?? Promise.resolve(false),
Comment thread
elkaix marked this conversation as resolved.
insertText: (text) => provider?.broadcast(Events.InsertMention, { mention: text }),
logError,
}),
);

log("Pythinker Code activated");
}

Expand Down
Loading
Loading