Add a Claude Code-like chat experience to the VS Code extension with: - Streaming chat panel (sidebar + editor tab) with markdown rendering - Tool use visualization with inline diffs (replace/with display) - Session history browser with JSONL transcript parsing - Thinking block indicator with elapsed time and token count - Clickable file paths that open in the editor - Permission mode setting (acceptEdits default) - Multi-turn conversation support via NDJSON stream-json protocol - Status bar with live activity indicators - Ctrl+Shift+L keybinding to open chat panel Made-with: Cursor Co-authored-by: henriquepasquini2 <henriquepasquini2@users.noreply.github.com>
187 lines
4.5 KiB
JavaScript
187 lines
4.5 KiB
JavaScript
/**
|
|
* NDJSON protocol helpers and message type constants for the OpenClaude
|
|
* stream-json SDK wire format.
|
|
*
|
|
* The extension spawns `openclaude --print --input-format=stream-json
|
|
* --output-format=stream-json` and speaks NDJSON over stdin/stdout.
|
|
* This module provides lightweight parsing, serialization, and type guards
|
|
* so the rest of the extension never touches raw JSON strings.
|
|
*/
|
|
|
|
const MESSAGE_TYPES = {
|
|
ASSISTANT: 'assistant',
|
|
USER: 'user',
|
|
USER_REPLAY: 'user_replay',
|
|
RESULT: 'result',
|
|
SYSTEM: 'system',
|
|
STREAM_EVENT: 'stream_event',
|
|
PARTIAL: 'partial',
|
|
COMPACT_BOUNDARY: 'compact_boundary',
|
|
STATUS: 'status',
|
|
API_RETRY: 'api_retry',
|
|
LOCAL_COMMAND_OUTPUT: 'local_command_output',
|
|
HOOK_STARTED: 'hook_started',
|
|
HOOK_PROGRESS: 'hook_progress',
|
|
HOOK_RESPONSE: 'hook_response',
|
|
TOOL_PROGRESS: 'tool_progress',
|
|
AUTH_STATUS: 'auth_status',
|
|
TASK_NOTIFICATION: 'task_notification',
|
|
TASK_STARTED: 'task_started',
|
|
TASK_PROGRESS: 'task_progress',
|
|
SESSION_STATE_CHANGED: 'session_state_changed',
|
|
FILES_PERSISTED: 'files_persisted',
|
|
TOOL_USE_SUMMARY: 'tool_use_summary',
|
|
RATE_LIMIT: 'rate_limit',
|
|
ELICITATION_COMPLETE: 'elicitation_complete',
|
|
PROMPT_SUGGESTION: 'prompt_suggestion',
|
|
STREAMLINED_TEXT: 'streamlined_text',
|
|
STREAMLINED_TOOL_USE_SUMMARY: 'streamlined_tool_use_summary',
|
|
POST_TURN_SUMMARY: 'post_turn_summary',
|
|
CONTROL_RESPONSE: 'control_response',
|
|
CONTROL_REQUEST: 'control_request',
|
|
};
|
|
|
|
function parseStdoutLine(line) {
|
|
const trimmed = (line || '').trim();
|
|
if (!trimmed) return null;
|
|
try {
|
|
return JSON.parse(trimmed);
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function serializeStdinMessage(msg) {
|
|
return JSON.stringify(msg) + '\n';
|
|
}
|
|
|
|
function buildUserMessage(text) {
|
|
return {
|
|
type: 'user',
|
|
message: {
|
|
role: 'user',
|
|
content: text,
|
|
},
|
|
parent_tool_use_id: null,
|
|
};
|
|
}
|
|
|
|
function buildControlResponse(requestId, result) {
|
|
return {
|
|
type: 'control_response',
|
|
response: {
|
|
subtype: 'success',
|
|
request_id: requestId,
|
|
response: result || {},
|
|
},
|
|
};
|
|
}
|
|
|
|
function isAssistantMessage(msg) {
|
|
return msg && msg.type === MESSAGE_TYPES.ASSISTANT;
|
|
}
|
|
|
|
function isPartialMessage(msg) {
|
|
return msg && msg.type === MESSAGE_TYPES.PARTIAL;
|
|
}
|
|
|
|
function isStreamEvent(msg) {
|
|
return msg && msg.type === MESSAGE_TYPES.STREAM_EVENT && msg.event;
|
|
}
|
|
|
|
function isContentBlockDelta(msg) {
|
|
return isStreamEvent(msg) && msg.event.type === 'content_block_delta';
|
|
}
|
|
|
|
function isContentBlockStart(msg) {
|
|
return isStreamEvent(msg) && msg.event.type === 'content_block_start';
|
|
}
|
|
|
|
function isMessageStart(msg) {
|
|
return isStreamEvent(msg) && msg.event.type === 'message_start';
|
|
}
|
|
|
|
function isMessageStop(msg) {
|
|
return isStreamEvent(msg) && msg.event.type === 'message_stop';
|
|
}
|
|
|
|
function isMessageDelta(msg) {
|
|
return isStreamEvent(msg) && msg.event.type === 'message_delta';
|
|
}
|
|
|
|
function isResultMessage(msg) {
|
|
return msg && msg.type === MESSAGE_TYPES.RESULT;
|
|
}
|
|
|
|
function isToolUse(block) {
|
|
return block && block.type === 'tool_use';
|
|
}
|
|
|
|
function isTextBlock(block) {
|
|
return block && block.type === 'text';
|
|
}
|
|
|
|
function isThinkingBlock(block) {
|
|
return block && block.type === 'thinking';
|
|
}
|
|
|
|
function isControlRequest(msg) {
|
|
return msg && msg.type === MESSAGE_TYPES.CONTROL_REQUEST;
|
|
}
|
|
|
|
function isStatusMessage(msg) {
|
|
return msg && msg.type === MESSAGE_TYPES.STATUS;
|
|
}
|
|
|
|
function isToolProgressMessage(msg) {
|
|
return msg && msg.type === MESSAGE_TYPES.TOOL_PROGRESS;
|
|
}
|
|
|
|
function isSessionStateChanged(msg) {
|
|
return msg && msg.type === MESSAGE_TYPES.SESSION_STATE_CHANGED;
|
|
}
|
|
|
|
function isRateLimitEvent(msg) {
|
|
return msg && msg.type === MESSAGE_TYPES.RATE_LIMIT;
|
|
}
|
|
|
|
function getTextContent(message) {
|
|
if (!message || !Array.isArray(message.content)) return '';
|
|
return message.content
|
|
.filter(b => b.type === 'text')
|
|
.map(b => b.text || '')
|
|
.join('');
|
|
}
|
|
|
|
function getToolUseBlocks(message) {
|
|
if (!message || !Array.isArray(message.content)) return [];
|
|
return message.content.filter(b => b.type === 'tool_use');
|
|
}
|
|
|
|
module.exports = {
|
|
MESSAGE_TYPES,
|
|
parseStdoutLine,
|
|
serializeStdinMessage,
|
|
buildUserMessage,
|
|
buildControlResponse,
|
|
isAssistantMessage,
|
|
isPartialMessage,
|
|
isStreamEvent,
|
|
isContentBlockDelta,
|
|
isContentBlockStart,
|
|
isMessageStart,
|
|
isMessageStop,
|
|
isMessageDelta,
|
|
isResultMessage,
|
|
isToolUse,
|
|
isTextBlock,
|
|
isThinkingBlock,
|
|
isControlRequest,
|
|
isStatusMessage,
|
|
isToolProgressMessage,
|
|
isSessionStateChanged,
|
|
isRateLimitEvent,
|
|
getTextContent,
|
|
getToolUseBlocks,
|
|
};
|