Skip to content

Commit 17967df

Browse files
authored
feat(vscode): editor-theme design system, config hub, and host UX (#48)
## Related Issue No linked issue — the problem is explained below. ## Problem The VS Code extension UI did not follow the editor color theme: it shipped a fixed dark palette with a bundled font, so light and high-contrast themes rendered incorrectly. Composer controls (permission mode, plan mode, thinking effort) were separate buttons that overlapped at narrow sidebar widths. Configuration was fragmented across three separate modals with no way to inspect the local config file or MCP connections. Host integration was minimal: no status bar presence, no code actions, no terminal/editor context menus, and long conversations rendered every message at once. ## What changed - **Editor-theme design system.** The webview token layer now derives every semantic color from `--vscode-*` variables via Tailwind `@theme inline`, with Dark+ fallbacks. Light, dark, and both high-contrast themes work with zero runtime theme detection; the only JS consumer is a single `useIsDark` hook backed by the host-maintained body classes. The bundled Inter font and the hardcoded `.dark` palette are removed. - **Config hub.** One page (rail + sections) replaces the three modals: models, providers, MCP servers, a read-only view of the local `config.toml` (with an open-in-editor action), and extension settings. Adapts to narrow sidebar widths. - **Consolidated composer menu.** Permission mode, plan mode, and thinking effort live in a single popover; approval prompts answer to number keys. Fixes the control overlap at narrow widths. - **Host integrations.** Status bar indicator with activity count, quick-fix code actions, terminal and editor context menu entries, a getting-started walkthrough, and a new-conversation keybinding. - **Chat fixes.** Long conversations render through a virtualized list (react-virtuoso, bottom-anchored); sign-out works from the command palette. - **Dependency hygiene.** Removed `@fontsource-variable/inter` and `react-scroll-to-bottom`; bumped `@vscode/test-electron` to ^3.1.0 (VS Code ≥ 1.132 renamed the macOS binary, which broke the extension-host smoke test). This approach fits the extension because VS Code already maintains the theme contract (`--vscode-*` variables and body classes) — deriving from it removes an entire parallel theming system instead of maintaining one. Verified locally: typecheck (tsc + tsgo, both tsconfigs), build, all 338 existing tests, type-aware lint, VSIX packaging audit, and an extension-host smoke test that activates the extension and opens the webview. ## Checklist - [x] I have read the [CONTRIBUTING](https://github.com/Pythoughts-labs/pythinker-code/blob/main/CONTRIBUTING.md) document. - [x] I have linked a related issue, or explained the problem above. - [ ] I have added tests that prove my feature works. (No new tests — this is UI/theming work; all 338 existing tests pass and the VSIX audit + extension-host smoke test cover activation.) - [x] Ran `gen-changesets` skill, or this PR needs no changeset. - [x] Ran `gen-docs` skill, or this PR needs no doc update. (Extension is a private package; no docs affected.) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added a unified configuration hub for models, providers, MCP servers, local configuration, and extension settings. * Added composer controls for permission, plan, and thinking modes. * Added VS Code integrations for terminal selections, quick fixes, context menus, walkthroughs, shortcuts, and status indicators. * Added conversation sorting, grouping, recent-session shortcuts, and context-usage visibility. * **Bug Fixes** * Improved long-conversation performance and fixed narrow-sidebar control overlap. * Enhanced approval and question dialogs with keyboard navigation and pending-request indicators. * **Style** * Improved light, dark, and high-contrast theme support across the interface. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent bceff21 commit 17967df

93 files changed

Lines changed: 2120 additions & 1290 deletions

File tree

Some content is hidden

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

.changeset/vscode-chat-fixes.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"pythinker-code": patch
3+
---
4+
5+
Render long conversations with a virtualized list, fix control overlap at narrow sidebar widths, and make sign-out work from the command palette.

.changeset/vscode-composer-menu.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"pythinker-code": minor
3+
---
4+
5+
Combine permission mode, plan mode, and thinking effort into one composer menu, and answer approval prompts with number keys.

.changeset/vscode-config-hub.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"pythinker-code": minor
3+
---
4+
5+
Add a config hub page that shows models, providers, MCP servers, the local config file, and extension settings in one place.

.changeset/vscode-editor-theme.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"pythinker-code": minor
3+
---
4+
5+
The extension UI now follows the editor color theme, including light, dark, and high-contrast themes.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"pythinker-code": minor
3+
---
4+
5+
Add a status bar indicator, quick-fix code actions, terminal and editor context menu entries, a getting-started walkthrough, and a new-conversation keybinding.

apps/vscode/package.json

Lines changed: 77 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,11 @@
122122
"title": "Pythinker Code: Insert Current File",
123123
"icon": "$(mention)"
124124
},
125+
{
126+
"command": "pythinker.addTerminalSelection",
127+
"title": "Pythinker Code: Add Terminal Selection to Pythinker",
128+
"icon": "$(terminal)"
129+
},
125130
{
126131
"command": "pythinker.newConversation",
127132
"title": "Pythinker Code: New Conversation",
@@ -154,6 +159,64 @@
154159
"key": "alt+k",
155160
"mac": "alt+k",
156161
"when": "editorTextFocus"
162+
},
163+
{
164+
"command": "pythinker.newConversation",
165+
"key": "ctrl+alt+n",
166+
"mac": "cmd+alt+n"
167+
}
168+
],
169+
"walkthroughs": [
170+
{
171+
"id": "pythinkerGettingStarted",
172+
"title": "Get started with Pythinker Code",
173+
"description": "Set up Pythinker Code and run your first conversation.",
174+
"steps": [
175+
{
176+
"id": "openView",
177+
"title": "Open the Pythinker view",
178+
"description": "Select the Pythinker icon in the Activity Bar to open the chat view.\n[Open Pythinker](command:pythinker.webview.focus)",
179+
"media": {
180+
"markdown": "walkthrough/open-view.md"
181+
},
182+
"completionEvents": [
183+
"onView:pythinker.webview"
184+
]
185+
},
186+
{
187+
"id": "signIn",
188+
"title": "Sign in or add a provider",
189+
"description": "Sign in with your Pythinker account, or add a model provider with your own API key.",
190+
"media": {
191+
"markdown": "walkthrough/sign-in.md"
192+
},
193+
"completionEvents": [
194+
"onContext:pythinker.isLoggedIn"
195+
]
196+
},
197+
{
198+
"id": "firstConversation",
199+
"title": "Run your first conversation",
200+
"description": "Send a request and learn how the approval modes control what the agent can do.\n[New Conversation](command:pythinker.newConversation)",
201+
"media": {
202+
"markdown": "walkthrough/first-conversation.md"
203+
},
204+
"completionEvents": [
205+
"onCommand:pythinker.newConversation"
206+
]
207+
},
208+
{
209+
"id": "referenceCode",
210+
"title": "Reference your code",
211+
"description": "Use @ mentions and Alt+K to point Pythinker at the exact files and lines you mean.",
212+
"media": {
213+
"markdown": "walkthrough/reference-code.md"
214+
},
215+
"completionEvents": [
216+
"onCommand:pythinker.insertMention"
217+
]
218+
}
219+
]
157220
}
158221
],
159222
"viewsContainers": {
@@ -208,6 +271,10 @@
208271
"command": "pythinker.insertMention",
209272
"when": "true"
210273
},
274+
{
275+
"command": "pythinker.addTerminalSelection",
276+
"when": "false"
277+
},
211278
{
212279
"command": "pythinker.newConversation",
213280
"when": "true"
@@ -228,8 +295,14 @@
228295
"editor/context": [
229296
{
230297
"command": "pythinker.insertMention",
231-
"group": "pythoughts",
232-
"when": "editorTextFocus"
298+
"group": "pythinker@1",
299+
"when": "editorHasSelection"
300+
}
301+
],
302+
"terminal/context": [
303+
{
304+
"command": "pythinker.addTerminalSelection",
305+
"group": "pythinker@1"
233306
}
234307
]
235308
}
@@ -259,12 +332,11 @@
259332
"@types/node": "^22.15.3",
260333
"@types/react": "^19.1.2",
261334
"@types/react-dom": "^19.1.2",
262-
"@types/react-scroll-to-bottom": "^4.2.5",
263335
"@types/react-syntax-highlighter": "^15.5.13",
264336
"@types/vscode": "1.100.0",
265337
"@vitejs/plugin-react": "^4.4.1",
266338
"@vscode/test-cli": "^0.0.11",
267-
"@vscode/test-electron": "^2.5.2",
339+
"@vscode/test-electron": "^3.1.0",
268340
"@vscode/vsce": "3.9.2",
269341
"acorn": "8.17.0",
270342
"ovsx": "1.0.2",
@@ -275,7 +347,6 @@
275347
},
276348
"dependencies": {
277349
"@base-ui/react": "^1.0.0",
278-
"@fontsource-variable/inter": "5.2.8",
279350
"@pythoughts/pythinker-code-sdk": "workspace:^",
280351
"@radix-ui/react-accordion": "^1.2.12",
281352
"@tabler/icons-react": "^3.36.0",
@@ -293,8 +364,8 @@
293364
"react": "^19.1.0",
294365
"react-dom": "^19.1.0",
295366
"react-markdown": "^10.1.0",
296-
"react-scroll-to-bottom": "^4.2.0",
297367
"react-syntax-highlighter": "^16.1.0",
368+
"react-virtuoso": "^4.18.11",
298369
"rehype-katex": "^7.0.1",
299370
"remark-gfm": "^4.0.1",
300371
"remark-math": "^6.0.0",

apps/vscode/shared/bridge.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ export const Methods = {
1919
Login: "login",
2020
Logout: "logout",
2121
SaveConfig: "saveConfig",
22+
GetConfigInfo: "getConfigInfo",
2223
GetExtensionConfig: "getExtensionConfig",
2324
SaveExtensionConfig: "saveExtensionConfig",
2425
OpenSettings: "openSettings",
@@ -103,7 +104,6 @@ export const Events = {
103104
InsertMention: "insertMention",
104105
NewConversation: "newConversation",
105106
FileChangesUpdated: "fileChangesUpdated",
106-
RollbackInput: "rollbackInput",
107107
} as const;
108108

109109
const rpcMethods = new Set<string>(Object.values(Methods));
@@ -142,6 +142,7 @@ function validateParams(method: RpcMethod, params: unknown): boolean {
142142
case Methods.CheckLoginStatus:
143143
case Methods.Login:
144144
case Methods.Logout:
145+
case Methods.GetConfigInfo:
145146
case Methods.GetExtensionConfig:
146147
case Methods.OpenSettings:
147148
case Methods.OpenFolder:

apps/vscode/shared/legacy-sdk.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,8 @@ export interface ModelConfig {
196196
name: string;
197197
provider: string;
198198
capabilities: string[];
199+
/** Maximum context size in tokens, from the model alias config (maxContextSize). */
200+
contextWindow?: number;
199201
adaptive_thinking?: boolean;
200202
support_efforts?: string[];
201203
default_effort?: string;

apps/vscode/shared/types.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,13 @@ export interface ExtensionConfig {
3636
version: string;
3737
}
3838

39+
/** The user's local config file, verbatim. Content is the raw on-disk text and may contain API keys. */
40+
export interface ConfigInfo {
41+
path: string | null;
42+
exists: boolean;
43+
content: string | null;
44+
}
45+
3946
export interface WorkspaceStatus {
4047
hasWorkspace: boolean;
4148
path?: string;

apps/vscode/src/PythinkerWebviewProvider.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ function getNonce(): string {
1919
export class PythinkerWebviewProvider implements vscode.WebviewViewProvider {
2020
private webviews = new Map<string, vscode.Webview>();
2121
private bridgeHandler: BridgeHandler;
22+
private sidebarView: vscode.WebviewView | undefined;
23+
private badgeCount = 0;
2224

2325
constructor(
2426
private readonly extensionUri: vscode.Uri,
@@ -51,13 +53,37 @@ export class PythinkerWebviewProvider implements vscode.WebviewViewProvider {
5153
resolveWebviewView(webviewView: vscode.WebviewView): void {
5254
const webviewId = `sidebar_${crypto.randomUUID()}`;
5355
this.setupWebview(webviewId, webviewView.webview);
56+
this.sidebarView = webviewView;
57+
this.applyBadge();
5458

5559
webviewView.onDidDispose(() => {
5660
void this.bridgeHandler.disposeView(webviewId);
5761
this.webviews.delete(webviewId);
62+
if (this.sidebarView === webviewView) this.sidebarView = undefined;
5863
});
5964
}
6065

66+
/** Shows `count` pending approval requests on the sidebar view; 0 clears the badge. */
67+
setSidebarBadge(count: number): void {
68+
this.badgeCount = count;
69+
this.applyBadge();
70+
}
71+
72+
private applyBadge(): void {
73+
const view = this.sidebarView;
74+
if (view === undefined) return;
75+
view.badge =
76+
this.badgeCount > 0
77+
? {
78+
value: this.badgeCount,
79+
tooltip:
80+
this.badgeCount === 1
81+
? "1 approval request waits for your decision"
82+
: `${this.badgeCount} approval requests wait for your decision`,
83+
}
84+
: undefined;
85+
}
86+
6187
createPanel(): vscode.WebviewPanel {
6288
const webviewId = `panel_${crypto.randomUUID()}`;
6389

0 commit comments

Comments
 (0)