mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-14 16:17:02 +00:00
fix(cursor): address review feedback and translator edge cases
This commit is contained in:
1 parent
d7b907ed9f
commit
27f6a675be
8 files changed
+338
-38
No files matched your search
@@ -1,5 +1,6 @@
|
||||
import type { CursorAuthStatus, CursorDaemonStatus, CursorModel } from '../cursor/types';
|
||||
import type { CursorConfig } from '../config/unified-config-types';
|
||||
import { getCcsDirDisplay } from '../utils/config-manager';
|
||||
import { color } from '../utils/ui';
|
||||
|
||||
function printLines(lines: string[]): void {
|
||||
@@ -47,6 +48,7 @@ export function renderCursorStatus(
|
||||
daemonStatus: CursorDaemonStatus
|
||||
): void {
|
||||
const localBaseUrl = `http://127.0.0.1:${cursorConfig.port}`;
|
||||
const dirDisplay = getCcsDirDisplay();
|
||||
const isReady =
|
||||
cursorConfig.enabled && authStatus.authenticated && !authStatus.expired && daemonStatus.running;
|
||||
|
||||
@@ -90,7 +92,7 @@ export function renderCursorStatus(
|
||||
console.log(` Models route: ${localBaseUrl}/v1/models`);
|
||||
console.log('');
|
||||
console.log('Client setup:');
|
||||
console.log(' Raw settings: ~/.ccs/cursor.settings.json');
|
||||
console.log(` Raw settings: ${dirDisplay}/cursor.settings.json`);
|
||||
console.log(' Subcommands: ccs cursor help');
|
||||
|
||||
if (isReady) {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/**
|
||||
* Cursor CLI Command
|
||||
*
|
||||
* Handles `ccs cursor <subcommand>` commands.
|
||||
* Handles `ccs cursor [subcommand]` commands.
|
||||
*/
|
||||
|
||||
import {
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { initUI, box, color, dim, sectionHeader, subheader } from '../utils/ui';
|
||||
import { isUnifiedMode } from '../config/unified-config-loader';
|
||||
import { getCcsDir, getCcsDirSource } from '../utils/config-manager';
|
||||
import { getCcsDirDisplay } from '../utils/config-manager';
|
||||
import { CLIPROXY_DEFAULT_PORT } from '../cliproxy/config/port-manager';
|
||||
import { getOfficialChannelsSupportMessage } from '../channels/official-channels-runtime';
|
||||
|
||||
@@ -129,8 +129,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
||||
writeLine('');
|
||||
|
||||
// Resolve display path for dynamic sections
|
||||
const [dirSource] = getCcsDirSource();
|
||||
const dirDisplay = dirSource === 'default' ? '~/.ccs' : getCcsDir();
|
||||
const dirDisplay = getCcsDirDisplay();
|
||||
|
||||
// Usage section
|
||||
writeLine(subheader('Usage:'));
|
||||
@@ -288,7 +287,7 @@ Run ${color('ccs config', 'command')} for web dashboard`.trim();
|
||||
'Auto-detects token from Cursor installation',
|
||||
],
|
||||
[
|
||||
['ccs cursor <cmd>', 'Use Cursor IDE integration'],
|
||||
['ccs cursor', 'Cursor status + local daemon connection details'],
|
||||
['ccs cursor auth', 'Import Cursor token'],
|
||||
['ccs cursor auth --manual --token <t> --machine-id <id>', 'Manual token import'],
|
||||
['ccs cursor status', 'Show connection status'],
|
||||
|
||||
@@ -55,6 +55,7 @@ interface OpenAIRequestBody {
|
||||
const MAX_TOOL_RESULT_CHARS = 12_000;
|
||||
const TOOL_RESULT_SERIALIZATION_FALLBACK = '[unserializable content]';
|
||||
const TOOL_USE_ARGUMENTS_FALLBACK = '{}';
|
||||
const TOOL_CALL_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
||||
|
||||
function isTextPart(part: OpenAIContentPart): part is OpenAITextPart {
|
||||
return part.type === 'text';
|
||||
@@ -68,19 +69,19 @@ function isToolResultPart(part: OpenAIContentPart): part is OpenAIToolResultPart
|
||||
return part.type === 'tool_result';
|
||||
}
|
||||
|
||||
function extractTextContent(content: OpenAIMessage['content']): string {
|
||||
function extractTextContent(content: OpenAIMessage['content'], separator = ''): string {
|
||||
if (typeof content === 'string') {
|
||||
return content;
|
||||
}
|
||||
|
||||
let text = '';
|
||||
const parts: string[] = [];
|
||||
for (const part of content) {
|
||||
if (isTextPart(part) && part.text) {
|
||||
text += part.text;
|
||||
parts.push(part.text);
|
||||
}
|
||||
}
|
||||
|
||||
return text;
|
||||
return parts.join(separator);
|
||||
}
|
||||
|
||||
function stringifyUnknown(value: unknown, fallback = ''): string {
|
||||
@@ -101,16 +102,16 @@ function truncateToolResultText(text: string): string {
|
||||
return text;
|
||||
}
|
||||
|
||||
let suffix = '\n[truncated]';
|
||||
let keepLength = Math.max(MAX_TOOL_RESULT_CHARS - suffix.length, 0);
|
||||
let omittedChars = text.length - keepLength;
|
||||
|
||||
suffix = `\n[truncated ${omittedChars} chars]`;
|
||||
keepLength = Math.max(MAX_TOOL_RESULT_CHARS - suffix.length, 0);
|
||||
omittedChars = text.length - keepLength;
|
||||
suffix = `\n[truncated ${omittedChars} chars]`;
|
||||
|
||||
return `${text.slice(0, keepLength)}${suffix}`;
|
||||
let omittedChars = text.length - MAX_TOOL_RESULT_CHARS;
|
||||
while (true) {
|
||||
const suffix = `\n[truncated ${omittedChars} chars]`;
|
||||
const keepLength = Math.max(MAX_TOOL_RESULT_CHARS - suffix.length, 0);
|
||||
const nextOmittedChars = text.length - keepLength;
|
||||
if (nextOmittedChars === omittedChars) {
|
||||
return `${text.slice(0, keepLength)}${suffix}`;
|
||||
}
|
||||
omittedChars = nextOmittedChars;
|
||||
}
|
||||
}
|
||||
|
||||
function escapeXml(text: string): string {
|
||||
@@ -133,6 +134,19 @@ function normalizeToolCallId(toolCallId: string | undefined): string {
|
||||
return typeof toolCallId === 'string' ? toolCallId.split('\n')[0] : '';
|
||||
}
|
||||
|
||||
function sanitizeToolCallId(toolCallId: string | undefined): string {
|
||||
const normalizedId = normalizeToolCallId(toolCallId).trim();
|
||||
if (!normalizedId) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (TOOL_CALL_ID_PATTERN.test(normalizedId)) {
|
||||
return normalizedId;
|
||||
}
|
||||
|
||||
return normalizedId.replace(/[^a-zA-Z0-9_-]/g, '');
|
||||
}
|
||||
|
||||
function extractToolResultText(content: unknown): string {
|
||||
if (content === undefined) {
|
||||
return '';
|
||||
@@ -146,7 +160,7 @@ function extractToolResultText(content: unknown): string {
|
||||
return content
|
||||
.filter(isTextPart)
|
||||
.map((part) => part.text || '')
|
||||
.join('');
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
return stringifyUnknown(content, TOOL_RESULT_SERIALIZATION_FALLBACK);
|
||||
@@ -161,9 +175,34 @@ function resolveToolUseId(
|
||||
messageIndex: number,
|
||||
partIndex: number
|
||||
): string {
|
||||
return typeof part.id === 'string' && part.id.length > 0
|
||||
? part.id
|
||||
: createFallbackToolUseId(messageIndex, partIndex);
|
||||
const sanitizedId = sanitizeToolCallId(part.id);
|
||||
return sanitizedId || createFallbackToolUseId(messageIndex, partIndex);
|
||||
}
|
||||
|
||||
function requireToolResultId(toolCallId: string | undefined, location: string): string {
|
||||
const sanitizedId = sanitizeToolCallId(toolCallId);
|
||||
if (sanitizedId) {
|
||||
return sanitizedId;
|
||||
}
|
||||
|
||||
throw new Error(`${location} must include a valid tool result id`);
|
||||
}
|
||||
|
||||
function normalizeAssistantToolCalls(
|
||||
toolCalls: NonNullable<OpenAIMessage['tool_calls']>,
|
||||
messageIndex: number
|
||||
): NonNullable<OpenAIMessage['tool_calls']> {
|
||||
return toolCalls.map((toolCall, toolCallIndex) => ({
|
||||
...toolCall,
|
||||
id: sanitizeToolCallId(toolCall.id) || createFallbackToolUseId(messageIndex, toolCallIndex),
|
||||
function: {
|
||||
name: toolCall.function?.name || 'tool',
|
||||
arguments:
|
||||
typeof toolCall.function?.arguments === 'string'
|
||||
? toolCall.function.arguments
|
||||
: stringifyUnknown(toolCall.function?.arguments ?? {}, TOOL_USE_ARGUMENTS_FALLBACK),
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
function rememberToolCallMeta(
|
||||
@@ -259,7 +298,8 @@ function mergeAssistantToolCalls(
|
||||
|
||||
function renderUserContent(
|
||||
content: OpenAIMessage['content'],
|
||||
toolCallMetaMap: Map<string, { name: string }>
|
||||
toolCallMetaMap: Map<string, { name: string }>,
|
||||
messageIndex: number
|
||||
): string {
|
||||
if (typeof content === 'string') {
|
||||
return content;
|
||||
@@ -267,7 +307,8 @@ function renderUserContent(
|
||||
|
||||
const parts: string[] = [];
|
||||
let textBuffer = '';
|
||||
for (const part of content) {
|
||||
for (let partIndex = 0; partIndex < content.length; partIndex++) {
|
||||
const part = content[partIndex];
|
||||
if (isTextPart(part) && part.text) {
|
||||
textBuffer += part.text;
|
||||
continue;
|
||||
@@ -282,7 +323,10 @@ function renderUserContent(
|
||||
textBuffer = '';
|
||||
}
|
||||
|
||||
const toolCallId = part.tool_use_id || '';
|
||||
const toolCallId = requireToolResultId(
|
||||
part.tool_use_id,
|
||||
`messages[${messageIndex}].content[${partIndex}]`
|
||||
);
|
||||
const normalizedId = normalizeToolCallId(toolCallId);
|
||||
const toolName =
|
||||
toolCallMetaMap.get(toolCallId)?.name || toolCallMetaMap.get(normalizedId)?.name || 'tool';
|
||||
@@ -317,7 +361,10 @@ function convertMessages(messages: OpenAIMessage[]): CursorMessage[] {
|
||||
}
|
||||
|
||||
if (msg.role === 'tool') {
|
||||
const toolCallId = msg.tool_call_id || '';
|
||||
const toolCallId = requireToolResultId(
|
||||
msg.tool_call_id,
|
||||
`messages[${messageIndex}].tool_call_id`
|
||||
);
|
||||
const normalizedToolCallId = normalizeToolCallId(toolCallId);
|
||||
const rememberedToolName =
|
||||
toolCallMetaMap.get(toolCallId)?.name || toolCallMetaMap.get(normalizedToolCallId)?.name;
|
||||
@@ -325,19 +372,20 @@ function convertMessages(messages: OpenAIMessage[]): CursorMessage[] {
|
||||
|
||||
result.push({
|
||||
role: 'user',
|
||||
content: buildToolResultBlock(toolName, toolCallId, extractTextContent(msg.content)),
|
||||
content: buildToolResultBlock(toolName, toolCallId, extractTextContent(msg.content, '\n')),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (msg.role === 'user' || msg.role === 'assistant') {
|
||||
if (msg.role === 'assistant') {
|
||||
const normalizedToolCalls = normalizeAssistantToolCalls(msg.tool_calls || [], messageIndex);
|
||||
const assistantToolCalls = mergeAssistantToolCalls(
|
||||
msg.tool_calls || [],
|
||||
normalizedToolCalls,
|
||||
extractToolCallsFromContent(msg.content, messageIndex)
|
||||
);
|
||||
if (msg.tool_calls?.length) {
|
||||
rememberToolCallMeta(toolCallMetaMap, msg.tool_calls);
|
||||
if (normalizedToolCalls.length > 0) {
|
||||
rememberToolCallMeta(toolCallMetaMap, normalizedToolCalls);
|
||||
}
|
||||
rememberToolUseParts(toolCallMetaMap, msg.content, messageIndex);
|
||||
|
||||
@@ -355,7 +403,7 @@ function convertMessages(messages: OpenAIMessage[]): CursorMessage[] {
|
||||
});
|
||||
}
|
||||
} else {
|
||||
const content = renderUserContent(msg.content, toolCallMetaMap);
|
||||
const content = renderUserContent(msg.content, toolCallMetaMap, messageIndex);
|
||||
if (content) {
|
||||
result.push({
|
||||
role: 'user',
|
||||
|
||||
@@ -102,6 +102,15 @@ export function getCcsDirSource(): [string, string] {
|
||||
return [r.source, r.dir];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the CCS directory as a user-facing display path.
|
||||
* Keeps the default path concise while preserving explicit overrides.
|
||||
*/
|
||||
export function getCcsDirDisplay(): string {
|
||||
const [source, dir] = getCcsDirSource();
|
||||
return source === 'default' ? '~/.ccs' : dir;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cloud sync folder patterns for security warning.
|
||||
*/
|
||||
|
||||
Reference in new issue
Block a user