Skip to content

feat(vscode): Ghost extension with chat, memories, and status bar#14

Merged
wcatz merged 2 commits intomainfrom
feat/vscode-extension
Mar 15, 2026
Merged

feat(vscode): Ghost extension with chat, memories, and status bar#14
wcatz merged 2 commits intomainfrom
feat/vscode-extension

Conversation

@wcatz
Copy link
Copy Markdown
Owner

@wcatz wcatz commented Mar 15, 2026

Summary

  • VSCode extension scaffold (vscode-ghost/) — Phase 4 of the TUI/extension plan
  • Chat panel: SSE streaming from ghost serve, tool progress spinners, inline approval buttons, token usage, multi-line input with Enter/Shift+Enter
  • Memory browser: project selector, debounced FTS search, category/importance display, delete
  • Status bar: connection health (15s poll), mode display, token counts
  • Ghost client: typed HTTP + SSE client wrapping all /api/v1/ endpoints
  • 5 commands: ghost.sendMessage, ghost.newSession, ghost.setMode, ghost.searchMemories, ghost.showChat
  • CSP-locked webviews (nonce scripts), native VSCode CSS variables for theme matching

Files

File Purpose
src/ghost-client.ts HTTP + SSE client for ghost serve API
src/chat-panel.ts Chat webview provider with streaming
src/memory-panel.ts Memory browser webview provider
src/status-bar.ts Status bar item (mode, tokens, health)
src/extension.ts Activation, commands, config wiring
media/chat.css Shared styles using VSCode CSS variables
media/ghost-icon.svg Activity bar icon

Test plan

  • npm run compile — zero errors
  • Load in VSCode Extension Host (F5) with ghost serve running
  • Verify chat streaming, approval flow, memory browse/search/delete

Summary by CodeRabbit

Release Notes

  • New Features
    • Introduced Ghost VS Code extension with an integrated chat interface for real-time conversations
    • Added memory and project management panel for organizing and searching memories
    • Session creation and mode switching capabilities
    • Status bar indicator displaying connection status and current mode
    • Support for streaming responses with token usage tracking
    • Configurable server URL and authentication settings

Activity bar with two webview panels:
- Chat: streaming SSE responses, tool progress indicators, inline
  approval buttons, token usage display, multi-line input
- Memories: project selector, FTS search, category/importance display,
  delete support

Plus:
- ghost-client.ts: HTTP + SSE client for all ghost serve endpoints
- status-bar.ts: connection status, mode display, token counts
- 5 commands: sendMessage, newSession, setMode, searchMemories, showChat
- Auto health check every 15s with reconnect
- Native VSCode CSS variables for theme matching
- CSP-locked webviews with nonce-based script execution
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 15, 2026

📝 Walkthrough

Walkthrough

Introduces a complete VSCode extension for Ghost integration, encompassing project configuration, a TypeScript client with comprehensive API operations and SSE-like streaming support, webview-based UI panels for chat and memory management, a status bar component, extension lifecycle management, and command handlers.

Changes

Cohort / File(s) Summary
Configuration & Build
vscode-ghost/.gitignore, vscode-ghost/tsconfig.json, vscode-ghost/package.json
Adds extension manifest, TypeScript configuration (ES2022, strict mode), and build/dev dependencies; .gitignore excludes build artifacts and extensions.
Core Client
vscode-ghost/src/ghost-client.ts
Implements GhostClient class with session management, memory operations, project listing, and streaming message support via HTTP SSE-like protocol with event emission and error handling.
UI Components
vscode-ghost/src/chat-panel.ts, vscode-ghost/src/memory-panel.ts, vscode-ghost/src/status-bar.ts
ChatPanelProvider handles chat UI, message streaming, and user actions; MemoryPanelProvider manages memory search and deletion; GhostStatusBar displays connection status, mode, and token usage.
Extension Entry Point
vscode-ghost/src/extension.ts
Implements extension activation/deactivation, client initialization, command registration (sendMessage, newSession, setMode, searchMemories, showChat), configuration watching, periodic health checks, and UI state synchronization.
Chat Styling
vscode-ghost/media/chat.css
Defines comprehensive CSS for chat UI using VSCode theme tokens, including message layouts (user/assistant/thinking/error), status bar, input area, animations, and responsive styling.

Sequence Diagram

sequenceDiagram
    actor User as User/VSCode UI
    participant Chat as ChatPanelProvider
    participant Client as GhostClient
    participant Server as Ghost Server

    User->>Chat: Send message via webview
    Chat->>Client: sendMessage(sessionId, text)
    Client->>Server: HTTP POST /sessions/{id}/messages
    Server-->>Client: SSE stream initiated
    
    loop Streaming Events
        Server-->>Client: text_delta / thinking_delta / tool_* / approval events
        Client->>Chat: Emit typed events
        Chat->>User: postMessage (update webview with streamed content)
        User->>User: Render incremental response
    end
    
    Server-->>Client: done event
    Client->>Chat: Emit done event
    Chat->>User: postMessage (mark message complete)
    Note over Chat,User: Abort available during streaming
Loading

Estimated Code Review Effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 Through VSCode's halls hops a Ghost so keen,
With chat panels glowing and memories pristine,
A rabbit's own extension, now coded with care,
Streaming and searching through digital air! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main feature being introduced: a VS Code extension with chat, memories, and status bar components.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/vscode-extension
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (1)
vscode-ghost/src/ghost-client.ts (1)

288-341: Consider adding request timeout to prevent indefinite hangs.

The request helper has no timeout configured. If the Ghost server becomes unresponsive, requests will hang indefinitely, potentially blocking VS Code operations.

⏱️ Proposed fix to add timeout
+    const TIMEOUT_MS = 30000; // 30 seconds
+
     const req = mod.request(options, (res) => {
       // ... existing handler
     });

+    req.setTimeout(TIMEOUT_MS, () => {
+      req.destroy(new Error("Request timeout"));
+    });
+
     req.on("error", reject);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vscode-ghost/src/ghost-client.ts` around lines 288 - 341, The request helper
(private request) currently can hang indefinitely; add a request timeout
constant (e.g., REQUEST_TIMEOUT_MS = 5000) and wire it into the outgoing request
by using req.setTimeout(REQUEST_TIMEOUT_MS, ...) to abort the request on
timeout; in the timeout callback destroy the request/connection (req.destroy())
and reject the promise with a clear Error like "Request timed out", and ensure
this timeout path can't also resolve/reject later (i.e., bail out after
rejecting). Update any tests or callers if they expect long-running requests.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@vscode-ghost/media/chat.css`:
- Around line 71-77: The .message CSS uses the deprecated `word-break:
break-word`; replace it with non-deprecated overflow-wrap rules (e.g., set
`overflow-wrap: anywhere;` and ensure `word-break: normal;`) so long words still
wrap; also update the same inline style in the memory panel to use overflow-wrap
instead of `word-break: break-word` to satisfy Stylelint.

In `@vscode-ghost/package.json`:
- Around line 73-76: The package exposes the ghost.autoStart setting but nothing
reads it; update the extension activation logic (activate) to read
workspace.getConfiguration('ghost').get('autoStart') and, if true and Ghost is
not running (use/is-create or existing isGhostRunning() helper), call the
existing startGhostServe() function to start it; also listen for configuration
changes (workspace.onDidChangeConfiguration) for the 'ghost.autoStart' key and
start/stop accordingly so the setting has effect. Ensure you reference the
existing symbols activate, startGhostServe, and isGhostRunning (or the
equivalent start/stop helpers) when implementing this behavior.

In `@vscode-ghost/src/chat-panel.ts`:
- Around line 293-303: addToolIndicator currently keys indicators by tool name
which causes tool_end events to affect all indicators with the same name; change
addToolIndicator(name, status) to accept the tool's unique id (e.g.,
addToolIndicator(id, name, status)), set div.dataset.toolId = id (instead of or
in addition to dataset.toolName), and update any code that marks indicators
finished (the tool_end handling and the similar block at the other location) to
find/update elements by data-tool-id (matching the event.id) rather than by name
to ensure lifecycle events match the exact tool instance.
- Around line 24-26: setClient currently only replaces this.client and leaves
this.session pointing to the old daemon; update setClient (in class ChatPanel)
to clear/reset the active session when swapping backends by
nulling/undefined-ing this.session (or calling a resetSession helper) whenever
the provided GhostClient differs or when ghost.serverUrl/ghost.authToken change
so future sends don't reuse a stale session id; reference setClient,
this.client, this.session and any send/openSession methods to ensure the session
reset is effective.
- Around line 91-98: When a session is created and when usage data arrives you
need to update the status bar: after this.client.createSession(...) and before
postMessage(...) call the GhostStatusBar APIs (e.g. statusBar.setMode(...)) to
set the chat mode and pass any session token info via
statusBar.setTokenInfo(...); likewise, in the block that forwards usage data to
the webview (the usage handling around lines 157-165) call
statusBar.setTokenInfo(...) with the usage/token counts so the status bar shows
tokens. Locate the code using createSession and the usage-forwarding block and
add calls to the existing GhostStatusBar methods (setMode and setTokenInfo) to
keep the status bar in sync with this.session and usage updates.
- Around line 315-320: The Enter keydown handler on inputEl currently calls
send() even during IME composition; update the handler to guard against
composition by checking KeyboardEvent.isComposing (or maintain an isComposing
flag via compositionstart/compositionend listeners) and only call send() when
composition is not active; specifically modify the
inputEl.addEventListener('keydown', ...) callback to return early if
e.isComposing (or if the isComposing flag is true) so pressing Enter to confirm
CJK input does not trigger send().

In `@vscode-ghost/src/extension.ts`:
- Around line 37-44: The command registered in
registerCommand("ghost.sendMessage") posts a message with type
"send_from_command" which chat-panel.ts never handles and will be dropped if the
webview isn't ready; modify the command to either post a message type the
webview already handles (e.g., the existing send message type handled in
chat-panel.ts) or update chat-panel.ts to add a handler for "send_from_command"
in its onDidReceiveMessage/message switch, and ensure the webview is
revealed/created before posting (call/open the chat panel or check
chatProvider.isActive/resolved) so postMessage is not a no-op.
- Around line 47-64: The various commands (ghost.newSession, ghost.setMode,
etc.) create or mutate sessions directly on GhostClient while ChatPanelProvider
keeps its own cached this.session causing divergent session usage; fix by
centralizing the active session (e.g., an exported/shared activeSessionId or an
ExtensionState object) and update the ChatPanelProvider whenever the session
changes: after client.createSession (in the ghost.newSession command) set the
centralized session id and call a new ChatPanelProvider.setSession(session) or
emit an event so the panel updates its this.session and
statusBar.setMode(session.mode); ensure ghost.setMode and other commands
read/update the centralized session (instead of listSessions()[0]) so all parts
of the extension use the same authoritative session id.
- Line 11: Change the immutable client binding so configuration updates actually
replace the instance used everywhere: make the top-level declaration a mutable
variable (replace const client = createClient() with let client =
createClient()), and in the configuration change handler assign the created
newClient back to that variable (client = newClient). Also ensure any long-lived
closures (registered command handlers and the 15-second health poll) reference
the outer mutable client variable (or call a getter that returns the current
client) instead of closing over the original instance so they pick up the
reassigned client.

In `@vscode-ghost/src/ghost-client.ts`:
- Around line 154-208: The stream handler currently discards any remaining
partial data held in the buffer when res emits "end"; on res.on("end") you
should process the leftover buffer (using the same parsing logic that handles
"event: " and "data: " lines and currentEvent) before emitting
emitter.emit("close"), attempting to parse any complete event fragments,
emitting the corresponding typed events via emitter (including
error/approval/done/etc.), and logging or emitting an error for
malformed/partial JSON instead of silently dropping it; refer to the buffer
variable, currentEvent, the res.on("data") parsing switch, and emitter to
integrate this final flush.
- Around line 323-327: The code currently swallows JSON.parse errors by
resolving with "undefined as T" (see the resolve(JSON.parse(data) as T) /
resolve(undefined as T) snippet); change the catch to reject the Promise with
the parse error (include the original data and the parse error message in the
Error) so callers get a failed Promise instead of an unexpected undefined;
update any callers (e.g., health() or other places that await this promise) to
handle the rejected promise or try/catch accordingly.

---

Nitpick comments:
In `@vscode-ghost/src/ghost-client.ts`:
- Around line 288-341: The request helper (private request) currently can hang
indefinitely; add a request timeout constant (e.g., REQUEST_TIMEOUT_MS = 5000)
and wire it into the outgoing request by using
req.setTimeout(REQUEST_TIMEOUT_MS, ...) to abort the request on timeout; in the
timeout callback destroy the request/connection (req.destroy()) and reject the
promise with a clear Error like "Request timed out", and ensure this timeout
path can't also resolve/reject later (i.e., bail out after rejecting). Update
any tests or callers if they expect long-running requests.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3272392f-64f6-4938-b5ec-917b68f0fc54

📥 Commits

Reviewing files that changed from the base of the PR and between 5d5fa7d and 50a19fe.

⛔ Files ignored due to path filters (2)
  • vscode-ghost/media/ghost-icon.svg is excluded by !**/*.svg
  • vscode-ghost/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (9)
  • vscode-ghost/.gitignore
  • vscode-ghost/media/chat.css
  • vscode-ghost/package.json
  • vscode-ghost/src/chat-panel.ts
  • vscode-ghost/src/extension.ts
  • vscode-ghost/src/ghost-client.ts
  • vscode-ghost/src/memory-panel.ts
  • vscode-ghost/src/status-bar.ts
  • vscode-ghost/tsconfig.json

Comment on lines +71 to +77
.message {
padding: 6px 10px;
border-radius: 6px;
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
max-width: 95%;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Replace deprecated word-break: break-word.

Line 76 is already failing Stylelint, and the same deprecated keyword also appears in the memory panel's inline CSS. overflow-wrap is the non-deprecated way to keep long message content inside the bubble.

💡 Suggested change
 .message {
   padding: 6px 10px;
   border-radius: 6px;
   line-height: 1.5;
   white-space: pre-wrap;
-  word-break: break-word;
+  overflow-wrap: anywhere;
   max-width: 95%;
 }
🧰 Tools
🪛 Stylelint (17.4.0)

[error] 76-76: Unexpected deprecated keyword "break-word" for property "word-break" (declaration-property-value-keyword-no-deprecated)

(declaration-property-value-keyword-no-deprecated)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vscode-ghost/media/chat.css` around lines 71 - 77, The .message CSS uses the
deprecated `word-break: break-word`; replace it with non-deprecated
overflow-wrap rules (e.g., set `overflow-wrap: anywhere;` and ensure
`word-break: normal;`) so long words still wrap; also update the same inline
style in the memory panel to use overflow-wrap instead of `word-break:
break-word` to satisfy Stylelint.

Comment thread vscode-ghost/package.json
Comment on lines +73 to +76
"ghost.autoStart": {
"type": "boolean",
"default": false,
"description": "Automatically start ghost serve if not running"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

ghost.autoStart is currently a dead setting.

The manifest exposes this option, but none of the provided extension code reads or reacts to it. Users can enable it without any behavior change.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vscode-ghost/package.json` around lines 73 - 76, The package exposes the
ghost.autoStart setting but nothing reads it; update the extension activation
logic (activate) to read workspace.getConfiguration('ghost').get('autoStart')
and, if true and Ghost is not running (use/is-create or existing
isGhostRunning() helper), call the existing startGhostServe() function to start
it; also listen for configuration changes (workspace.onDidChangeConfiguration)
for the 'ghost.autoStart' key and start/stop accordingly so the setting has
effect. Ensure you reference the existing symbols activate, startGhostServe, and
isGhostRunning (or the equivalent start/stop helpers) when implementing this
behavior.

Comment on lines +24 to +26
public setClient(client: GhostClient): void {
this.client = client;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reset the active session when setClient() swaps backends.

Line 25 only replaces the transport. If ghost.serverUrl or ghost.authToken changes, this.session still points at the old daemon, so the next send reuses a stale session id.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vscode-ghost/src/chat-panel.ts` around lines 24 - 26, setClient currently
only replaces this.client and leaves this.session pointing to the old daemon;
update setClient (in class ChatPanel) to clear/reset the active session when
swapping backends by nulling/undefined-ing this.session (or calling a
resetSession helper) whenever the provided GhostClient differs or when
ghost.serverUrl/ghost.authToken change so future sends don't reuse a stale
session id; reference setClient, this.client, this.session and any
send/openSession methods to ensure the session reset is effective.

Comment on lines +91 to +98
try {
this.session = await this.client.createSession(
folders[0].uri.fsPath
);
this.postMessage({
type: "session",
session: this.session,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

The chat lifecycle never updates GhostStatusBar.

vscode-ghost/src/status-bar.ts already exposes setMode() and setTokenInfo(), but this provider only forwards session and usage data into the webview. With the current wiring, opening chat first leaves the status bar mode blank, and token counts never appear there at all.

Also applies to: 157-165

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vscode-ghost/src/chat-panel.ts` around lines 91 - 98, When a session is
created and when usage data arrives you need to update the status bar: after
this.client.createSession(...) and before postMessage(...) call the
GhostStatusBar APIs (e.g. statusBar.setMode(...)) to set the chat mode and pass
any session token info via statusBar.setTokenInfo(...); likewise, in the block
that forwards usage data to the webview (the usage handling around lines
157-165) call statusBar.setTokenInfo(...) with the usage/token counts so the
status bar shows tokens. Locate the code using createSession and the
usage-forwarding block and add calls to the existing GhostStatusBar methods
(setMode and setTokenInfo) to keep the status bar in sync with this.session and
usage updates.

Comment on lines +293 to +303
function addToolIndicator(name, status) {
const div = document.createElement('div');
div.className = 'tool-indicator ' + status;
div.dataset.toolName = name;
div.innerHTML = '<span class="tool-icon">' +
(status === 'running' ? '&#9881;' : '&#10003;') +
'</span> ' + name;
messagesEl.appendChild(div);
scrollToBottom();
return div;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Match tool lifecycle events by id, not by name.

The backend supplies both fields, but the UI keys indicators by name. If the same tool runs twice, tool_end will mark every matching spinner as done.

💡 Suggested change
-    function addToolIndicator(name, status) {
+    function addToolIndicator(id, name, status) {
       const div = document.createElement('div');
       div.className = 'tool-indicator ' + status;
-      div.dataset.toolName = name;
+      div.dataset.toolId = id;
       div.innerHTML = '<span class="tool-icon">' +
         (status === 'running' ? '&#9881;' : '&#10003;') +
         '</span> ' + name;
       messagesEl.appendChild(div);
@@
         case 'tool_start':
-          addToolIndicator(msg.name, 'running');
+          addToolIndicator(msg.id, msg.name, 'running');
           break;
@@
           const indicators = messagesEl.querySelectorAll('.tool-indicator.running');
           indicators.forEach(el => {
-            if (el.dataset.toolName === msg.name) {
+            if (el.dataset.toolId === msg.id) {
               el.className = 'tool-indicator done';
               el.querySelector('.tool-icon').innerHTML = '&#10003;';
             }
           });

Also applies to: 378-389

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vscode-ghost/src/chat-panel.ts` around lines 293 - 303, addToolIndicator
currently keys indicators by tool name which causes tool_end events to affect
all indicators with the same name; change addToolIndicator(name, status) to
accept the tool's unique id (e.g., addToolIndicator(id, name, status)), set
div.dataset.toolId = id (instead of or in addition to dataset.toolName), and
update any code that marks indicators finished (the tool_end handling and the
similar block at the other location) to find/update elements by data-tool-id
(matching the event.id) rather than by name to ensure lifecycle events match the
exact tool instance.

let healthInterval: ReturnType<typeof setInterval>;

export function activate(context: vscode.ExtensionContext): void {
const client = createClient();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Reassign the captured client on configuration changes.

Only the providers and status bar receive newClient. Every registered command and the 15-second health poll still close over the original instance from Line 11, so changing ghost.serverUrl or ghost.authToken does not fully take effect until the extension reloads.

💡 Suggested change
 export function activate(context: vscode.ExtensionContext): void {
-  const client = createClient();
+  let client = createClient();
@@
       if (
         e.affectsConfiguration("ghost.serverUrl") ||
         e.affectsConfiguration("ghost.authToken")
       ) {
-        const newClient = createClient();
-        chatProvider.setClient(newClient);
-        memoryProvider.setClient(newClient);
-        statusBar.setClient(newClient);
+        client = createClient();
+        chatProvider.setClient(client);
+        memoryProvider.setClient(client);
+        statusBar.setClient(client);
       }
     })
   );

Also applies to: 137-154

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vscode-ghost/src/extension.ts` at line 11, Change the immutable client
binding so configuration updates actually replace the instance used everywhere:
make the top-level declaration a mutable variable (replace const client =
createClient() with let client = createClient()), and in the configuration
change handler assign the created newClient back to that variable (client =
newClient). Also ensure any long-lived closures (registered command handlers and
the 15-second health poll) reference the outer mutable client variable (or call
a getter that returns the current client) instead of closing over the original
instance so they pick up the reassigned client.

Comment on lines +37 to +44
vscode.commands.registerCommand("ghost.sendMessage", async () => {
const text = await vscode.window.showInputBox({
prompt: "Message Ghost",
placeHolder: "Ask Ghost something...",
});
if (text) {
chatProvider.postMessage({ type: "send_from_command", text });
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

ghost.sendMessage is wired to a message type the webview never handles.

The command posts send_from_command, but the listener in vscode-ghost/src/chat-panel.ts has no case for that message. If the chat view has not been resolved yet, postMessage() is also a no-op, so this command currently can't deliver anything.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vscode-ghost/src/extension.ts` around lines 37 - 44, The command registered
in registerCommand("ghost.sendMessage") posts a message with type
"send_from_command" which chat-panel.ts never handles and will be dropped if the
webview isn't ready; modify the command to either post a message type the
webview already handles (e.g., the existing send message type handled in
chat-panel.ts) or update chat-panel.ts to add a handler for "send_from_command"
in its onDidReceiveMessage/message switch, and ensure the webview is
revealed/created before posting (call/open the chat panel or check
chatProvider.isActive/resolved) so postMessage is not a no-op.

Comment on lines +47 to +64
vscode.commands.registerCommand("ghost.newSession", async () => {
const folders = vscode.workspace.workspaceFolders;
if (!folders || folders.length === 0) {
vscode.window.showErrorMessage("No workspace folder open.");
return;
}
try {
const session = await client.createSession(
folders[0].uri.fsPath
);
vscode.window.showInformationMessage(
`Ghost session started: ${session.project_name}`
);
statusBar.setMode(session.mode);
} catch (err) {
vscode.window.showErrorMessage(`Failed to start session: ${err}`);
}
}),
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Share one authoritative session id across commands and the chat view.

These commands talk to GhostClient directly while ChatPanelProvider keeps its own cached this.session. After ghost.newSession, the panel still talks to its old session (or creates another one later), and ghost.setMode updates listSessions()[0], which can be a different session once more than one exists.

Also applies to: 66-91

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vscode-ghost/src/extension.ts` around lines 47 - 64, The various commands
(ghost.newSession, ghost.setMode, etc.) create or mutate sessions directly on
GhostClient while ChatPanelProvider keeps its own cached this.session causing
divergent session usage; fix by centralizing the active session (e.g., an
exported/shared activeSessionId or an ExtensionState object) and update the
ChatPanelProvider whenever the session changes: after client.createSession (in
the ghost.newSession command) set the centralized session id and call a new
ChatPanelProvider.setSession(session) or emit an event so the panel updates its
this.session and statusBar.setMode(session.mode); ensure ghost.setMode and other
commands read/update the centralized session (instead of listSessions()[0]) so
all parts of the extension use the same authoritative session id.

Comment on lines +154 to +208
let buffer = "";
let currentEvent = "";

res.on("data", (chunk: Buffer) => {
buffer += chunk.toString();
const lines = buffer.split("\n");
buffer = lines.pop() || "";

for (const line of lines) {
if (line.startsWith("event: ")) {
currentEvent = line.slice(7).trim();
} else if (line.startsWith("data: ")) {
const dataStr = line.slice(6);
try {
const data = JSON.parse(dataStr);
const event: StreamEvent = { type: currentEvent, data };
emitter.emit("event", event);

// Emit typed events for convenience.
switch (currentEvent) {
case "text":
emitter.emit("text", data.text || "");
break;
case "thinking":
emitter.emit("thinking", data.text || "");
break;
case "tool_use_start":
emitter.emit("tool_start", data);
break;
case "tool_input_delta":
emitter.emit("tool_delta", data);
break;
case "tool_use_end":
emitter.emit("tool_end", data);
break;
case "approval_required":
emitter.emit("approval", data as ApprovalRequest);
break;
case "done":
emitter.emit("done", data);
break;
case "error":
emitter.emit("error", new Error(data.error || "unknown"));
break;
}
} catch {
// Skip malformed JSON lines.
}
}
}
});

res.on("end", () => {
emitter.emit("close");
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Unprocessed buffer data on stream end.

If the stream ends with incomplete data in the buffer (e.g., network interruption mid-event), that data is silently discarded. While SSE events should always terminate with \n\n, network issues could cause partial data.

Consider processing or logging the remaining buffer before emitting close:

🛠️ Suggested improvement
       res.on("end", () => {
+        if (buffer.trim()) {
+          // Log or handle incomplete SSE data if needed for debugging
+          console.warn("SSE stream ended with unprocessed data:", buffer);
+        }
         emitter.emit("close");
       });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let buffer = "";
let currentEvent = "";
res.on("data", (chunk: Buffer) => {
buffer += chunk.toString();
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (line.startsWith("event: ")) {
currentEvent = line.slice(7).trim();
} else if (line.startsWith("data: ")) {
const dataStr = line.slice(6);
try {
const data = JSON.parse(dataStr);
const event: StreamEvent = { type: currentEvent, data };
emitter.emit("event", event);
// Emit typed events for convenience.
switch (currentEvent) {
case "text":
emitter.emit("text", data.text || "");
break;
case "thinking":
emitter.emit("thinking", data.text || "");
break;
case "tool_use_start":
emitter.emit("tool_start", data);
break;
case "tool_input_delta":
emitter.emit("tool_delta", data);
break;
case "tool_use_end":
emitter.emit("tool_end", data);
break;
case "approval_required":
emitter.emit("approval", data as ApprovalRequest);
break;
case "done":
emitter.emit("done", data);
break;
case "error":
emitter.emit("error", new Error(data.error || "unknown"));
break;
}
} catch {
// Skip malformed JSON lines.
}
}
}
});
res.on("end", () => {
emitter.emit("close");
});
let buffer = "";
let currentEvent = "";
res.on("data", (chunk: Buffer) => {
buffer += chunk.toString();
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (line.startsWith("event: ")) {
currentEvent = line.slice(7).trim();
} else if (line.startsWith("data: ")) {
const dataStr = line.slice(6);
try {
const data = JSON.parse(dataStr);
const event: StreamEvent = { type: currentEvent, data };
emitter.emit("event", event);
// Emit typed events for convenience.
switch (currentEvent) {
case "text":
emitter.emit("text", data.text || "");
break;
case "thinking":
emitter.emit("thinking", data.text || "");
break;
case "tool_use_start":
emitter.emit("tool_start", data);
break;
case "tool_input_delta":
emitter.emit("tool_delta", data);
break;
case "tool_use_end":
emitter.emit("tool_end", data);
break;
case "approval_required":
emitter.emit("approval", data as ApprovalRequest);
break;
case "done":
emitter.emit("done", data);
break;
case "error":
emitter.emit("error", new Error(data.error || "unknown"));
break;
}
} catch {
// Skip malformed JSON lines.
}
}
}
});
res.on("end", () => {
if (buffer.trim()) {
// Log or handle incomplete SSE data if needed for debugging
console.warn("SSE stream ended with unprocessed data:", buffer);
}
emitter.emit("close");
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vscode-ghost/src/ghost-client.ts` around lines 154 - 208, The stream handler
currently discards any remaining partial data held in the buffer when res emits
"end"; on res.on("end") you should process the leftover buffer (using the same
parsing logic that handles "event: " and "data: " lines and currentEvent) before
emitting emitter.emit("close"), attempting to parse any complete event
fragments, emitting the corresponding typed events via emitter (including
error/approval/done/etc.), and logging or emitting an error for
malformed/partial JSON instead of silently dropping it; refer to the buffer
variable, currentEvent, the res.on("data") parsing switch, and emitter to
integrate this final flush.

Comment on lines +323 to +327
try {
resolve(JSON.parse(data) as T);
} catch {
resolve(undefined as T);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Type safety issue: silent undefined fallback on JSON parse failure.

When JSON parsing fails on a successful response, the method returns undefined as T, which bypasses TypeScript's type checking. Callers expecting a valid object (e.g., health() expecting { status, version }) will receive undefined, potentially causing runtime errors.

🐛 Proposed fix to reject on parse failure
           try {
             resolve(JSON.parse(data) as T);
           } catch {
-            resolve(undefined as T);
+            // Empty response is valid for void-returning methods like DELETE
+            if (data.trim() === "") {
+              resolve(undefined as T);
+            } else {
+              reject(new Error(`Invalid JSON response: ${data.substring(0, 100)}`));
+            }
           }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
try {
resolve(JSON.parse(data) as T);
} catch {
resolve(undefined as T);
}
try {
resolve(JSON.parse(data) as T);
} catch {
// Empty response is valid for void-returning methods like DELETE
if (data.trim() === "") {
resolve(undefined as T);
} else {
reject(new Error(`Invalid JSON response: ${data.substring(0, 100)}`));
}
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@vscode-ghost/src/ghost-client.ts` around lines 323 - 327, The code currently
swallows JSON.parse errors by resolving with "undefined as T" (see the
resolve(JSON.parse(data) as T) / resolve(undefined as T) snippet); change the
catch to reject the Promise with the parse error (include the original data and
the parse error message in the Error) so callers get a failed Promise instead of
an unexpected undefined; update any callers (e.g., health() or other places that
await this promise) to handle the rejected promise or try/catch accordingly.

@wcatz wcatz merged commit 162f542 into main Mar 15, 2026
4 checks passed
@wcatz wcatz deleted the feat/vscode-extension branch March 15, 2026 02:56
wcatz added a commit that referenced this pull request Mar 24, 2026
Address CodeQL path-injection alerts (#12, #13, #14):
- ClaudeMemoryDir: validate resolved path stays under ~/.claude/projects/
- ParseMemoryFile: filepath.Clean on input path
- importFromDir: filepath.Clean on dir, filepath.Base on entry names
wcatz added a commit that referenced this pull request Mar 24, 2026
Address CodeQL path-injection alerts (#12, #13, #14):
- ClaudeMemoryDir: validate resolved path stays under ~/.claude/projects/
- ParseMemoryFile: filepath.Clean on input path
- importFromDir: filepath.Clean on dir, filepath.Base on entry names
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant