feat(vscode): Ghost extension with chat, memories, and status bar#14
feat(vscode): Ghost extension with chat, memories, and status bar#14
Conversation
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
📝 WalkthroughWalkthroughIntroduces 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
Sequence DiagramsequenceDiagram
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
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches
🧪 Generate unit tests (beta)
📝 Coding Plan
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. Comment |
There was a problem hiding this comment.
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
requesthelper 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
⛔ Files ignored due to path filters (2)
vscode-ghost/media/ghost-icon.svgis excluded by!**/*.svgvscode-ghost/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (9)
vscode-ghost/.gitignorevscode-ghost/media/chat.cssvscode-ghost/package.jsonvscode-ghost/src/chat-panel.tsvscode-ghost/src/extension.tsvscode-ghost/src/ghost-client.tsvscode-ghost/src/memory-panel.tsvscode-ghost/src/status-bar.tsvscode-ghost/tsconfig.json
| .message { | ||
| padding: 6px 10px; | ||
| border-radius: 6px; | ||
| line-height: 1.5; | ||
| white-space: pre-wrap; | ||
| word-break: break-word; | ||
| max-width: 95%; |
There was a problem hiding this comment.
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.
| "ghost.autoStart": { | ||
| "type": "boolean", | ||
| "default": false, | ||
| "description": "Automatically start ghost serve if not running" |
There was a problem hiding this comment.
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.
| public setClient(client: GhostClient): void { | ||
| this.client = client; | ||
| } |
There was a problem hiding this comment.
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.
| try { | ||
| this.session = await this.client.createSession( | ||
| folders[0].uri.fsPath | ||
| ); | ||
| this.postMessage({ | ||
| type: "session", | ||
| session: this.session, | ||
| }); |
There was a problem hiding this comment.
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.
| 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' ? '⚙' : '✓') + | ||
| '</span> ' + name; | ||
| messagesEl.appendChild(div); | ||
| scrollToBottom(); | ||
| return div; | ||
| } |
There was a problem hiding this comment.
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' ? '⚙' : '✓') +
'</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 = '✓';
}
});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(); |
There was a problem hiding this comment.
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.
| 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 }); | ||
| } |
There was a problem hiding this comment.
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.
| 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}`); | ||
| } | ||
| }), |
There was a problem hiding this comment.
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.
| 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"); | ||
| }); |
There was a problem hiding this comment.
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.
| 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.
| try { | ||
| resolve(JSON.parse(data) as T); | ||
| } catch { | ||
| resolve(undefined as T); | ||
| } |
There was a problem hiding this comment.
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.
| 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.
Summary
vscode-ghost/) — Phase 4 of the TUI/extension planghost serve, tool progress spinners, inline approval buttons, token usage, multi-line input with Enter/Shift+Enter/api/v1/endpointsghost.sendMessage,ghost.newSession,ghost.setMode,ghost.searchMemories,ghost.showChatFiles
src/ghost-client.tssrc/chat-panel.tssrc/memory-panel.tssrc/status-bar.tssrc/extension.tsmedia/chat.cssmedia/ghost-icon.svgTest plan
npm run compile— zero errorsF5) withghost serverunningSummary by CodeRabbit
Release Notes