Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion webapp/_webapp/src/components/loading-indicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ export const LoadingIndicator = ({ text = "Thinking", estimatedSeconds = 0, erro

// Get status message based on phase
const getStatusMessage = () => {
if (isTimeout) return "Sorry — this request took too long to complete. We're working on improving reliability. You can try waiting a bit longer or refreshing the page. Thanks for your patience.";
if (isTimeout)
return "Sorry — this request took too long to complete. We're working on improving reliability. You can try waiting a bit longer or refreshing the page. Thanks for your patience.";
if (phase === "orange") return "Synthesizing...";
if (phase === "red") return "Just a moment...";
if (errorMessage && errorMessage.length > 0) return errorMessage;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const JsonRpc = ({ functionName, jsonRpcResult, preparing, animated }: Js
>
<svg
className={cn("w-4 h-4 transition-transform duration-200", {
"rotate-180": !isCollapsed
"rotate-180": !isCollapsed,
})}
fill="none"
stroke="currentColor"
Expand All @@ -49,24 +49,22 @@ export const JsonRpc = ({ functionName, jsonRpcResult, preparing, animated }: Js
</svg>
</button>
</div>

<div className={cn("canselect overflow-hidden transition-all duration-300 ease-in-out", {
"max-h-0 opacity-0": isCollapsed,
"max-h-[1000px] opacity-100": !isCollapsed
})}>

<div
className={cn("canselect overflow-hidden transition-all duration-300 ease-in-out", {
"max-h-0 opacity-0": isCollapsed,
"max-h-[1000px] opacity-100": !isCollapsed,
})}
>
{jsonRpcResult.result && (
<div className="text-xs">
<MarkdownComponent animated={animated}>
{jsonRpcResult.result.content?.map((content) => content.text).join("\n") || ""}
</MarkdownComponent>
</div>
)}

{jsonRpcResult.error && (
<div className="text-xs text-red-600">
{jsonRpcResult.error.message}
{jsonRpcResult.result.content?.map((content) => content.text).join("\n") || ""}
</MarkdownComponent>
</div>
)}

{jsonRpcResult.error && <div className="text-xs text-red-600">{jsonRpcResult.error.message}</div>}
</div>
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,21 @@ export default function Tools({ messageId, functionName, message, error, prepari
} else if (functionName === "always_exception") {
return <AlwaysExceptionCard message={message} preparing={preparing} animated={animated} />;
} else if (XTRA_MCP_TOOL_NAMES.includes(functionName)) {
return <JsonRpc functionName={functionName} jsonRpcResult={jsonRpcResult || UNKNOWN_JSONRPC_RESULT} preparing={preparing} animated={animated} />;
return (
<JsonRpc
functionName={functionName}
jsonRpcResult={jsonRpcResult || UNKNOWN_JSONRPC_RESULT}
preparing={preparing}
animated={animated}
/>
);
}

// fallback to unknown tool card if the json rpc result is not defined
if (jsonRpcResult) {
return <JsonRpc functionName={functionName} jsonRpcResult={jsonRpcResult} preparing={preparing} animated={animated} />;
return (
<JsonRpc functionName={functionName} jsonRpcResult={jsonRpcResult} preparing={preparing} animated={animated} />
Copy link

Copilot AI Dec 7, 2025

Choose a reason for hiding this comment

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

[nitpick] Inconsistent JSX formatting: This line should be formatted across multiple lines like the similar JSX element above (lines 49-56) for consistency.

Suggested change
<JsonRpc functionName={functionName} jsonRpcResult={jsonRpcResult} preparing={preparing} animated={animated} />
<JsonRpc
functionName={functionName}
jsonRpcResult={jsonRpcResult}
preparing={preparing}
animated={animated}
/>

Copilot uses AI. Check for mistakes.
);
} else {
return <UnknownToolCard functionName={functionName} message={message} animated={animated} />;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,83 +1,91 @@
export type JsonRpcResult = {
jsonrpc: string;
id: number;
result?: {
content: Array<{
type: string;
text: string;
}>;
};
error?: {
code: number;
message: string;
}
}
jsonrpc: string;
id: number;
result?: {
content: Array<{
type: string;
text: string;
}>;
};
error?: {
code: number;
message: string;
};
};

export const UNKNOWN_JSONRPC_RESULT: JsonRpcResult = {
jsonrpc: "2.0",
id: -1,
error: {
code: -1,
message: "Unknown JSONRPC result",
},
}
jsonrpc: "2.0",
id: -1,
error: {
code: -1,
message: "Unknown JSONRPC result",
},
};

const isValidJsonRpcResult = (obj: any): obj is JsonRpcResult => {
// Check if obj is an object and not null
if (typeof obj !== 'object' || obj === null) {
return false;
}
// Check if obj is an object and not null
if (typeof obj !== "object" || obj === null) {
return false;
}

// Check required properties
if (typeof obj.jsonrpc !== 'string' || typeof obj.id !== 'number') {
return false;
}
// Check required properties
if (typeof obj.jsonrpc !== "string" || typeof obj.id !== "number") {
return false;
}

// Check that either result or error is present (but not both required)
const hasResult = obj.result !== undefined;
const hasError = obj.error !== undefined;
// Check that either result or error is present (but not both required)
const hasResult = obj.result !== undefined;
const hasError = obj.error !== undefined;

// Validate result structure if present
if (hasResult) {
if (typeof obj.result !== 'object' || obj.result === null) {
return false;
}
if (obj.result.content !== undefined) {
if (!Array.isArray(obj.result.content)) {
return false;
}
// Validate each content item
for (const item of obj.result.content) {
if (typeof item !== 'object' || item === null ||
typeof item.type !== 'string' || typeof item.text !== 'string') {
return false;
}
}
// Validate result structure if present
if (hasResult) {
if (typeof obj.result !== "object" || obj.result === null) {
return false;
}
if (obj.result.content !== undefined) {
if (!Array.isArray(obj.result.content)) {
return false;
}
// Validate each content item
for (const item of obj.result.content) {
if (
typeof item !== "object" ||
item === null ||
typeof item.type !== "string" ||
typeof item.text !== "string"
) {
return false;
}
}
}
}

// Validate error structure if present
if (hasError) {
if (typeof obj.error !== 'object' || obj.error === null ||
typeof obj.error.code !== 'number' || typeof obj.error.message !== 'string') {
return false;
}
// Validate error structure if present
if (hasError) {
if (
typeof obj.error !== "object" ||
obj.error === null ||
typeof obj.error.code !== "number" ||
typeof obj.error.message !== "string"
) {
return false;
}
}

return true;
return true;
};

export const parseJsonRpcResult = (message: string): JsonRpcResult | undefined => {
try {
const json = JSON.parse(message);

// Validate the structure before casting
if (isValidJsonRpcResult(json)) {
return json;
}

return undefined;
} catch (error) {
return undefined;
try {
const json = JSON.parse(message);

// Validate the structure before casting
if (isValidJsonRpcResult(json)) {
return json;
}
}

return undefined;
} catch (error) {
return undefined;
}
};
1 change: 0 additions & 1 deletion webapp/_webapp/src/index.css
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ body {
@apply font-medium text-gray-500;
}


/* 相邻 tool-card 的样式处理 */
.tool-card + .tool-card {
/* 相邻的第二个卡片:移除上边框,调整上圆角,减少上边距,减少上 padding */
Expand Down
2 changes: 1 addition & 1 deletion webapp/_webapp/src/libs/overleaf-socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -245,7 +245,7 @@ export async function wsConnect(
const wsUrl = import.meta.env.DEV ? "ws://localhost:3000" : "wss://" + currentDomain;
const data = await res.text();
const sessionId = data.split(":")[0];

// Set cookies for WebSocket connection
document.cookie = `overleaf_session2=${overleafAuth.cookieOverleafSession2}; path=/; domain=${import.meta.env.DEV ? "localhost" : currentDomain}`;
document.cookie = `GCLB=${overleafAuth.cookieGCLB}; path=/; domain=${import.meta.env.DEV ? "localhost" : currentDomain}`;
Expand Down
21 changes: 11 additions & 10 deletions webapp/_webapp/src/views/login/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,20 +14,21 @@ export const Login = () => {

return (
<div className="flex flex-col h-full w-full items-center justify-center bg-gray-50 noselect py-12">
{!showEndpointSettings && <>
<Logo className="mb-4" />
<div className="flex flex-col items-center justify-center">
<p className="text-exo-2 text-2xl font-light mb-2">Welcome to</p>
<div>
<span className="text-exo-2 text-2xl font-light">Paper</span>
<span className="text-exo-2 text-2xl font-bold">Debugger</span>
{!showEndpointSettings && (
<>
<Logo className="mb-4" />
<div className="flex flex-col items-center justify-center">
<p className="text-exo-2 text-2xl font-light mb-2">Welcome to</p>
<div>
<span className="text-exo-2 text-2xl font-light">Paper</span>
<span className="text-exo-2 text-2xl font-bold">Debugger</span>
</div>
</div>
</div>
</>}
</>
)}
{showEndpointSettings && <AdvancedSettings />}
<div className="flex-1"></div>


<LoginWithGoogle
isLoginLoading={isLoginLoading}
setIsLoginLoading={setIsLoginLoading}
Expand Down
1 change: 0 additions & 1 deletion webapp/_webapp/src/views/settings/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import { SettingsHeader } from "./settings-header";
import { UserDeveloperTools } from "./sections/user-developer-tools";
import { AccountSettings } from "./sections/account-settings";
import { UISettings } from "./sections/ui-settings";
import { BetaFeatureSettings } from "./sections/beta-feature-settings";
import { RealDeveloperTools } from "./sections/real-developer-tools";
import { SettingsFooter } from "./sections/footer";

Expand Down
Loading