Unverified 提交 beeaef57 authored 作者: Will Chen's avatar Will Chen 提交者: GitHub

Replace ChatLogsData with comprehensive SessionDebugBundle schema (#2488)

## Summary - Replaces the old `ChatLogsDataSchema` with a new `SessionDebugBundleSchema` (schema version 1) that captures all non-sensitive data needed for debugging chat sessions - Each message now includes AI SDK JSON (with base64 images stripped), model name, token usage, timestamps, commit hashes, request ID, and approval state - Adds non-sensitive user settings snapshot, app metadata, custom provider/model definitions, and MCP server configurations to the debug bundle - Updates HelpDialog to use the new schema and adds `Session Schema: v2.0` marker to GitHub issue templates ## Test plan - [ ] Verify `npm run ts` passes (type-check) - [ ] Verify `npm run lint` passes - [ ] Verify `npm test` passes (all 661 unit tests) - [ ] Manual: Open Help dialog → Upload Chat Session → verify the review modal shows messages, codebase, logs, and system info correctly - [ ] Manual: Complete upload and verify the GitHub issue template includes `Session Schema: v2.0` - [ ] Manual: Download uploaded JSON and verify it contains: `schemaVersion`, `system`, `settings`, `app`, `chat` (with `aiMessagesJson` per message), `providers`, `mcpServers`, `codebase`, `logs` - [ ] Manual: Verify no API keys, OAuth tokens, or MCP env vars appear in uploaded data 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- devin-review-badge-begin --> --- <a href="https://app.devin.ai/review/dyad-sh/dyad/pull/2488"> <picture> <source media="(prefers-color-scheme: dark)" srcset="https://static.devin.ai/assets/gh-open-in-devin-review-dark.svg?v=1"> <img src="https://static.devin.ai/assets/gh-open-in-devin-review-light.svg?v=1" alt="Open with Devin"> </picture> </a> <!-- devin-review-badge-end --> <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Expands what gets captured and uploaded for support sessions (settings/app metadata/providers/MCP and richer message data), so any sanitization or schema mismatch could leak data or break the support upload flow. > > **Overview** > Switches chat-session uploads from `ChatLogsData` to a new versioned `SessionDebugBundle` (`SESSION_DEBUG_SCHEMA_VERSION = 2`) and updates the IPC contract/exports accordingly. > > The main-process handler now assembles a comprehensive bundle (system/runtime info, sanitized settings snapshot, app metadata, full chat messages with stripped image/file blobs, custom provider/model summaries, MCP server configs without env/header secrets, codebase snapshot, and last ~1000 log lines), and `HelpDialog` is updated to fetch/review/upload this bundle and annotate created GitHub issues with `Session Schema: v2.0` (plus adds a settings section to the bug report template). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 7b94199939aa6963787e8fe69716e20cd6570b7d. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: 's avatarClaude Opus 4.5 <noreply@anthropic.com>
上级 fd917781
......@@ -21,7 +21,7 @@ import { ipc } from "@/ipc/types";
import { useState, useEffect } from "react";
import { useAtomValue } from "jotai";
import { selectedChatIdAtom } from "@/atoms/chatAtoms";
import { ChatLogsData } from "@/ipc/types";
import { SessionDebugBundle } from "@/ipc/types";
import { showError } from "@/lib/toast";
import { HelpBotDialog } from "./HelpBotDialog";
import { useSettings } from "@/hooks/useSettings";
......@@ -37,7 +37,9 @@ export function HelpDialog({ isOpen, onClose }: HelpDialogProps) {
const [isLoading, setIsLoading] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const [reviewMode, setReviewMode] = useState(false);
const [chatLogsData, setChatLogsData] = useState<ChatLogsData | null>(null);
const [debugBundle, setDebugBundle] = useState<SessionDebugBundle | null>(
null,
);
const [uploadComplete, setUploadComplete] = useState(false);
const [sessionId, setSessionId] = useState("");
const [isHelpBotOpen, setIsHelpBotOpen] = useState(false);
......@@ -52,7 +54,7 @@ export function HelpDialog({ isOpen, onClose }: HelpDialogProps) {
setIsLoading(false);
setIsUploading(false);
setReviewMode(false);
setChatLogsData(null);
setDebugBundle(null);
setUploadComplete(false);
setSessionId("");
};
......@@ -76,6 +78,20 @@ export function HelpDialog({ isOpen, onClose }: HelpDialogProps) {
const debugInfo = await ipc.system.getSystemDebugInfo();
// Create a formatted issue body with the debug info
const settingsLines = settings
? [
`- Selected Model: ${settings.selectedModel?.provider}:${settings.selectedModel?.name}`,
`- Chat Mode: ${settings.selectedChatMode ?? "default"}`,
`- Auto Approve Changes: ${settings.autoApproveChanges ?? "n/a"}`,
`- Dyad Pro Enabled: ${settings.enableDyadPro ?? "n/a"}`,
`- Thinking Budget: ${settings.thinkingBudget ?? "n/a"}`,
`- Runtime Mode: ${settings.runtimeMode2 ?? "n/a"}`,
`- Release Channel: ${settings.releaseChannel ?? "n/a"}`,
`- Auto Fix Problems: ${settings.enableAutoFixProblems ?? "n/a"}`,
`- Native Git: ${settings.enableNativeGit ?? "n/a"}`,
].join("\n")
: "Settings not available";
const issueBody = `
<!-- Please fill in all fields in English -->
......@@ -96,6 +112,9 @@ export function HelpDialog({ isOpen, onClose }: HelpDialogProps) {
- Telemetry ID: ${debugInfo.telemetryId || "n/a"}
- Model: ${debugInfo.selectedLanguageModel || "n/a"}
## Settings
${settingsLines}
## Logs
\`\`\`
${debugInfo.logs.slice(-3_500) || "No logs available"}
......@@ -131,10 +150,10 @@ ${debugInfo.logs.slice(-3_500) || "No logs available"}
setIsUploading(true);
try {
// Get chat logs (includes debug info, chat data, and codebase)
const chatLogs = await ipc.misc.getChatLogs(selectedChatId);
const debugBundle = await ipc.misc.getSessionDebugBundle(selectedChatId);
// Store data for review and switch to review mode
setChatLogsData(chatLogs);
setDebugBundle(debugBundle);
setReviewMode(true);
} catch (error) {
console.error("Failed to upload chat session:", error);
......@@ -147,17 +166,10 @@ ${debugInfo.logs.slice(-3_500) || "No logs available"}
};
const handleSubmitChatLogs = async () => {
if (!chatLogsData) return;
if (!debugBundle) return;
setIsUploading(true);
try {
// Prepare data for upload
const chatLogsJson = {
systemInfo: chatLogsData.debugInfo,
chat: chatLogsData.chat,
codebaseSnippet: chatLogsData.codebase,
};
// Get signed URL
const response = await fetch(
"https://upload-logs.dyad.sh/generate-upload-url",
......@@ -180,10 +192,11 @@ ${debugInfo.logs.slice(-3_500) || "No logs available"}
const { uploadUrl, filename } = await response.json();
// Upload the full debug bundle directly
await ipc.system.uploadToSignedUrl({
url: uploadUrl,
contentType: "application/json",
data: chatLogsJson,
data: debugBundle,
});
// Extract session ID (filename without extension)
......@@ -201,7 +214,7 @@ ${debugInfo.logs.slice(-3_500) || "No logs available"}
const handleCancelReview = () => {
setReviewMode(false);
setChatLogsData(null);
setDebugBundle(null);
};
const handleOpenGitHubIssue = () => {
......@@ -210,6 +223,7 @@ ${debugInfo.logs.slice(-3_500) || "No logs available"}
<!-- Please fill in all fields in English -->
Session ID: ${sessionId}
Session Schema: v2.0
Pro User ID: ${userBudget?.redactedUserId || "n/a"}
## Issue Description (required)
......@@ -276,7 +290,7 @@ Pro User ID: ${userBudget?.redactedUserId || "n/a"}
);
}
if (reviewMode && chatLogsData) {
if (reviewMode && debugBundle) {
return (
<Dialog open={isOpen} onOpenChange={handleClose}>
<DialogContent className="max-w-4xl max-h-[80vh] overflow-hidden flex flex-col">
......@@ -302,7 +316,7 @@ Pro User ID: ${userBudget?.redactedUserId || "n/a"}
<div className="border rounded-md p-3">
<h3 className="font-medium mb-2">Chat Messages</h3>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto">
{chatLogsData.chat.messages.map((msg) => (
{debugBundle.chat.messages.map((msg) => (
<div key={msg.id} className="mb-2">
<span className="font-semibold">
{msg.role === "user" ? "You" : "Assistant"}:{" "}
......@@ -316,29 +330,63 @@ Pro User ID: ${userBudget?.redactedUserId || "n/a"}
<div className="border rounded-md p-3">
<h3 className="font-medium mb-2">Codebase Snapshot</h3>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto font-mono">
{chatLogsData.codebase}
{debugBundle.codebase}
</div>
</div>
<div className="border rounded-md p-3">
<h3 className="font-medium mb-2">Logs</h3>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto font-mono">
{chatLogsData.debugInfo.logs}
{debugBundle.logs}
</div>
</div>
<div className="border rounded-md p-3">
<h3 className="font-medium mb-2">System Information</h3>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-32 overflow-y-auto">
<p>Dyad Version: {chatLogsData.debugInfo.dyadVersion}</p>
<p>Platform: {chatLogsData.debugInfo.platform}</p>
<p>Architecture: {chatLogsData.debugInfo.architecture}</p>
<p>Dyad Version: {debugBundle.system.dyadVersion}</p>
<p>Platform: {debugBundle.system.platform}</p>
<p>Architecture: {debugBundle.system.architecture}</p>
<p>
Node Version:{" "}
{chatLogsData.debugInfo.nodeVersion || "Not available"}
{debugBundle.system.nodeVersion || "Not available"}
</p>
</div>
</div>
<details className="border rounded-md p-3">
<summary className="font-medium cursor-pointer">Settings</summary>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto mt-2 font-mono whitespace-pre-wrap">
{JSON.stringify(debugBundle.settings, null, 2)}
</div>
</details>
<details className="border rounded-md p-3">
<summary className="font-medium cursor-pointer">
App Metadata
</summary>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto mt-2 font-mono whitespace-pre-wrap">
{JSON.stringify(debugBundle.app, null, 2)}
</div>
</details>
<details className="border rounded-md p-3">
<summary className="font-medium cursor-pointer">
Custom Providers & Models
</summary>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto mt-2 font-mono whitespace-pre-wrap">
{JSON.stringify(debugBundle.providers, null, 2)}
</div>
</details>
<details className="border rounded-md p-3">
<summary className="font-medium cursor-pointer">
MCP Servers
</summary>
<div className="text-sm bg-slate-50 dark:bg-slate-900 rounded p-2 max-h-40 overflow-y-auto mt-2 font-mono whitespace-pre-wrap">
{JSON.stringify(debugBundle.mcpServers, null, 2)}
</div>
</details>
</div>
<div className="flex justify-between mt-4 pt-2 sticky bottom-0 bg-background">
......
......@@ -3,8 +3,11 @@ import { platform, arch } from "os";
import { readSettings } from "../../main/settings";
import { createTypedHandler } from "./base";
import { systemContracts } from "../types/system";
import { miscContracts } from "../types/misc";
import { miscContracts, SESSION_DEBUG_SCHEMA_VERSION } from "../types/misc";
import type { SystemDebugInfo } from "../types/system";
import type { SessionDebugBundle } from "../types/misc";
import type { UserSettings } from "@/lib/schemas";
import type { AiMessagesJsonV6 } from "../../db/schema";
import log from "electron-log";
import path from "path";
......@@ -12,10 +15,15 @@ import fs from "fs";
import { runShellCommand } from "../utils/runShellCommand";
import { extractCodebase } from "../../utils/codebase";
import { db } from "../../db";
import { chats, apps } from "../../db/schema";
import {
chats,
apps,
language_model_providers,
language_models,
mcpServers,
} from "../../db/schema";
import { eq } from "drizzle-orm";
import { getDyadAppPath } from "../../paths/paths";
import { LargeLanguageModel } from "@/lib/schemas";
import { validateChatContext } from "../utils/context_paths_utils";
// Shared function to get system debug info
......@@ -117,6 +125,140 @@ async function getSystemDebugInfo({
};
}
function serializeModelForDebug(model: {
provider: string;
name: string;
customModelId?: number;
}): string {
return `${model.provider}:${model.name} | customId: ${model.customModelId}`;
}
/**
* Extracts non-sensitive settings for the debug bundle.
* All Secret fields (API keys, OAuth tokens) are excluded.
* Provider setup status is derived as boolean flags only.
*/
function sanitizeSettingsForDebug(settings: UserSettings) {
// Build provider setup status: { providerName: hasApiKey }
const providerSetupStatus: Record<string, boolean> = {};
if (settings.providerSettings) {
for (const [provider, providerSetting] of Object.entries(
settings.providerSettings,
)) {
providerSetupStatus[provider] = !!providerSetting?.apiKey?.value;
}
}
return {
selectedModel: {
name: settings.selectedModel.name,
provider: settings.selectedModel.provider,
customModelId: settings.selectedModel.customModelId,
},
selectedChatMode: settings.selectedChatMode ?? null,
defaultChatMode: settings.defaultChatMode ?? null,
autoApproveChanges: settings.autoApproveChanges ?? null,
enableDyadPro: settings.enableDyadPro ?? null,
thinkingBudget: settings.thinkingBudget ?? null,
maxChatTurnsInContext: settings.maxChatTurnsInContext ?? null,
enableAutoFixProblems: settings.enableAutoFixProblems ?? null,
enableNativeGit: settings.enableNativeGit ?? null,
enableAutoUpdate: settings.enableAutoUpdate,
releaseChannel: settings.releaseChannel,
runtimeMode2: settings.runtimeMode2 ?? null,
zoomLevel: settings.zoomLevel ?? null,
previewDeviceMode: settings.previewDeviceMode ?? null,
enableProLazyEditsMode: settings.enableProLazyEditsMode ?? null,
proLazyEditsMode: settings.proLazyEditsMode ?? null,
enableProSmartFilesContextMode:
settings.enableProSmartFilesContextMode ?? null,
enableProWebSearch: settings.enableProWebSearch ?? null,
proSmartContextOption: settings.proSmartContextOption ?? null,
enableSupabaseWriteSqlMigration:
settings.enableSupabaseWriteSqlMigration ?? null,
agentToolConsents: settings.agentToolConsents ?? null,
experiments: settings.experiments
? Object.fromEntries(
Object.entries(settings.experiments).filter(
([, v]) => typeof v === "boolean",
),
)
: null,
customNodePath: settings.customNodePath ?? null,
providerSetupStatus,
};
}
/**
* Strips base64 image data from AI SDK messages JSON.
* Replaces image content with "[stripped]" and adds _strippedByteLength metadata.
* This keeps the bundle size manageable while preserving message structure.
*
* Works on the raw JSON representation to avoid tight coupling with AI SDK types.
*/
function stripImagesFromAiMessagesJson(json: AiMessagesJsonV6 | null): unknown {
if (!json || !json.messages) return json;
// Work on raw JSON to avoid AI SDK type constraints when modifying content
const raw = JSON.parse(JSON.stringify(json));
for (const msg of raw.messages) {
if (!Array.isArray(msg.content)) continue;
for (let i = 0; i < msg.content.length; i++) {
const part = msg.content[i];
if (
part.type === "image" &&
typeof part.image === "string" &&
part.image.length > 200
) {
msg.content[i] = {
...part,
_strippedByteLength: part.image.length,
image: "[stripped]",
};
} else if (
part.type === "file" &&
typeof part.data === "string" &&
part.data.length > 200
) {
msg.content[i] = {
...part,
_strippedByteLength: part.data.length,
data: "[stripped]",
};
}
}
}
return raw;
}
/**
* Reads application logs from the electron-log file.
*/
function readAppLogs(linesOfLogs: number, level: "warn" | "info"): string {
try {
const logPath = log.transports.file.getFile().path;
if (!fs.existsSync(logPath)) return "";
const logContent = fs.readFileSync(logPath, "utf8");
const logLines = logContent.split("\n").filter((line) => {
if (level === "info") return true;
const logLevelRegex = /\[.*?\] \[(\w+)\]/;
const match = line.match(logLevelRegex);
if (!match) return true;
const logLevel = match[1];
if (level === "warn") {
return logLevel === "warn" || logLevel === "error";
}
return true;
});
return logLines.slice(-linesOfLogs).join("\n");
} catch (err) {
console.error("Failed to read log file:", err);
return `Error reading logs: ${err}`;
}
}
export function registerDebugHandlers() {
createTypedHandler(systemContracts.getSystemDebugInfo, async () => {
console.log("IPC: get-system-debug-info called");
......@@ -126,18 +268,40 @@ export function registerDebugHandlers() {
});
});
createTypedHandler(miscContracts.getChatLogs, async (_, chatId) => {
console.log(`IPC: get-chat-logs called for chat ${chatId}`);
createTypedHandler(miscContracts.getSessionDebugBundle, async (_, chatId) => {
console.log(`IPC: get-session-debug-bundle called for chat ${chatId}`);
try {
// We can retrieve a lot more lines here because we're not limited by the
// GitHub issue URL length limit.
const debugInfo = await getSystemDebugInfo({
linesOfLogs: 1_000,
level: "info",
});
const settings = readSettings();
// Get Dyad version
const packageJsonPath = path.resolve(
__dirname,
"..",
"..",
"package.json",
);
let dyadVersion = "unknown";
try {
const packageJson = JSON.parse(
fs.readFileSync(packageJsonPath, "utf8"),
);
dyadVersion = packageJson.version;
} catch (err) {
console.error("Failed to read package.json:", err);
}
// Get chat data from database
// Get runtime info in parallel
const [nodeVersion, pnpmVersion, nodePathResult] = await Promise.all([
runShellCommand("node --version").catch(() => null),
runShellCommand("pnpm --version").catch(() => null),
(platform() === "win32"
? runShellCommand("where.exe node")
: runShellCommand("which node")
).catch(() => null),
]);
// Get chat with full messages from database
const chatRecord = await db.query.chats.findFirst({
where: eq(chats.id, chatId),
with: {
......@@ -151,18 +315,6 @@ export function registerDebugHandlers() {
throw new Error(`Chat with ID ${chatId} not found`);
}
// Format the chat to match the Chat interface
const chat = {
id: chatRecord.id,
title: chatRecord.title || "Untitled Chat",
messages: chatRecord.messages.map((msg) => ({
id: msg.id,
role: msg.role,
content: msg.content,
approvalState: msg.approvalState,
})),
};
// Get app data from database
const app = await db.query.apps.findFirst({
where: eq(apps.id, chatRecord.appId),
......@@ -172,22 +324,123 @@ export function registerDebugHandlers() {
throw new Error(`App with ID ${chatRecord.appId} not found`);
}
// Extract codebase
const appPath = getDyadAppPath(app.path);
const codebase = (
await extractCodebase({
appPath,
// Query custom providers, custom models, and MCP servers in parallel
const [customProviders, customModels, mcpServerRecords, codebase] =
await Promise.all([
db.select().from(language_model_providers),
db.select().from(language_models),
db.select().from(mcpServers),
extractCodebase({
appPath: getDyadAppPath(app.path),
chatContext: validateChatContext(app.chatContext),
})
).formattedOutput;
}).then((result) => result.formattedOutput),
]);
// Read logs
const logs = readAppLogs(1_000, "info");
// Build the bundle
const bundle: SessionDebugBundle = {
schemaVersion: SESSION_DEBUG_SCHEMA_VERSION,
exportedAt: new Date().toISOString(),
system: {
dyadVersion,
platform: process.platform,
architecture: arch(),
nodeVersion,
pnpmVersion,
nodePath: nodePathResult,
electronVersion: process.versions.electron ?? "unknown",
telemetryId:
settings.telemetryConsent === "opted_out"
? null
: settings.telemetryUserId || "unknown",
},
settings: sanitizeSettingsForDebug(settings),
app: {
id: app.id,
name: app.name,
path: app.path,
createdAt: app.createdAt.toISOString(),
updatedAt: app.updatedAt.toISOString(),
githubOrg: app.githubOrg,
githubRepo: app.githubRepo,
githubBranch: app.githubBranch,
supabaseProjectId: app.supabaseProjectId,
supabaseOrganizationSlug: app.supabaseOrganizationSlug,
neonProjectId: app.neonProjectId,
vercelProjectId: app.vercelProjectId,
vercelProjectName: app.vercelProjectName,
vercelDeploymentUrl: app.vercelDeploymentUrl,
installCommand: app.installCommand,
startCommand: app.startCommand,
chatContext: app.chatContext ?? null,
themeId: app.themeId,
},
chat: {
id: chatRecord.id,
appId: chatRecord.appId,
title: chatRecord.title,
initialCommitHash: chatRecord.initialCommitHash,
createdAt: chatRecord.createdAt.toISOString(),
messages: chatRecord.messages.map((msg) => ({
id: msg.id,
role: msg.role,
content: msg.content,
createdAt: msg.createdAt.toISOString(),
aiMessagesJson: stripImagesFromAiMessagesJson(
msg.aiMessagesJson ?? null,
),
model: msg.model ?? null,
totalTokens: msg.maxTokensUsed ?? null,
approvalState: msg.approvalState ?? null,
sourceCommitHash: msg.sourceCommitHash ?? null,
commitHash: msg.commitHash ?? null,
requestId: msg.requestId ?? null,
usingFreeAgentModeQuota: msg.usingFreeAgentModeQuota ?? null,
})),
},
providers: {
customProviders: customProviders.map((p) => ({
id: p.id,
name: p.name,
hasApiBaseUrl: !!p.api_base_url,
envVarName: p.env_var_name,
})),
customModels: customModels.map((m) => ({
id: m.id,
displayName: m.displayName,
apiName: m.apiName,
builtinProviderId: m.builtinProviderId,
customProviderId: m.customProviderId,
maxOutputTokens: m.max_output_tokens,
contextWindow: m.context_window,
})),
},
mcpServers: mcpServerRecords.map((s) => ({
id: s.id,
name: s.name,
transport: s.transport,
command: s.command,
args: s.args,
url: s.url,
enabled: s.enabled,
// envJson and headersJson intentionally excluded (may contain secrets)
})),
return {
debugInfo,
chat,
codebase,
logs,
};
return bundle;
} catch (error) {
console.error(`Error in get-chat-logs:`, error);
console.error(`Error in get-session-debug-bundle:`, error);
throw error;
}
});
......@@ -208,7 +461,3 @@ export function registerDebugHandlers() {
clipboard.writeImage(image);
});
}
function serializeModelForDebug(model: LargeLanguageModel): string {
return `${model.provider}:${model.name} | customId: ${model.customModelId}`;
}
......@@ -280,7 +280,12 @@ export type {
export type { SecurityReviewResult } from "./security";
// Misc types
export type { ChatLogsData, DeepLinkData, AppOutput, EnvVar } from "./misc";
export type {
SessionDebugBundle,
DeepLinkData,
AppOutput,
EnvVar,
} from "./misc";
// Free agent quota types
export type { FreeAgentQuotaStatus } from "./free_agent_quota";
......
......@@ -41,39 +41,241 @@ export const SetAppEnvVarsParamsSchema = z.object({
});
// =============================================================================
// Chat Logs Schemas
// Session Debug Bundle Schemas
// =============================================================================
export const ChatLogsDataSchema = z.object({
debugInfo: z.object({
nodeVersion: z.string().nullable(),
pnpmVersion: z.string().nullable(),
nodePath: z.string().nullable(),
telemetryId: z.string(),
telemetryConsent: z.string(),
telemetryUrl: z.string(),
/**
* Schema version for the session debug bundle format.
* Bump this when making breaking changes to the schema.
*/
export const SESSION_DEBUG_SCHEMA_VERSION = 2;
// -- System info --
const DebugSystemInfoSchema = z.object({
/** Dyad application version (from package.json) */
dyadVersion: z.string(),
/** OS platform: "darwin", "win32", "linux" */
platform: z.string(),
/** CPU architecture: "x64", "arm64" */
architecture: z.string(),
logs: z.string(),
selectedLanguageModel: z.string(),
/** Node.js version, or null if not found */
nodeVersion: z.string().nullable(),
/** pnpm version, or null if not found */
pnpmVersion: z.string().nullable(),
/** Resolved path to the node binary, or null */
nodePath: z.string().nullable(),
/** Electron version */
electronVersion: z.string(),
/** Telemetry ID for cross-referencing server-side logs. Null if user opted out. */
telemetryId: z.string().nullable(),
});
// -- Non-sensitive settings snapshot --
const DebugSettingsSchema = z.object({
/** Currently selected language model */
selectedModel: z.object({
name: z.string(),
provider: z.string(),
customModelId: z.number().optional(),
}),
chat: z.object({
/** Active chat mode for the session */
selectedChatMode: z.string().nullable(),
/** Default chat mode preference */
defaultChatMode: z.string().nullable(),
/** Whether changes are auto-approved without review */
autoApproveChanges: z.boolean().nullable(),
/** Whether Dyad Pro is enabled */
enableDyadPro: z.boolean().nullable(),
/** Thinking budget level: "low" | "medium" | "high" */
thinkingBudget: z.string().nullable(),
/** Max chat turns kept in context window */
maxChatTurnsInContext: z.number().nullable(),
/** Whether auto-fix problems is enabled */
enableAutoFixProblems: z.boolean().nullable(),
/** Whether native git is enabled */
enableNativeGit: z.boolean().nullable(),
/** Whether auto-update is enabled */
enableAutoUpdate: z.boolean(),
/** Release channel: "stable" | "beta" */
releaseChannel: z.string(),
/** Runtime mode: "host" | "docker" */
runtimeMode2: z.string().nullable(),
/** UI zoom level */
zoomLevel: z.string().nullable(),
/** Preview device mode: "desktop" | "tablet" | "mobile" */
previewDeviceMode: z.string().nullable(),
/** Whether turbo edits mode is enabled */
enableProLazyEditsMode: z.boolean().nullable(),
/** Turbo edits mode variant: "off" | "v1" | "v2" */
proLazyEditsMode: z.string().nullable(),
/** Whether smart files context mode is enabled (Pro) */
enableProSmartFilesContextMode: z.boolean().nullable(),
/** Whether web search is enabled (Pro) */
enableProWebSearch: z.boolean().nullable(),
/** Smart context option: "balanced" | "conservative" | "deep" */
proSmartContextOption: z.string().nullable(),
/** Whether Supabase write SQL migration is enabled */
enableSupabaseWriteSqlMigration: z.boolean().nullable(),
/** Agent tool consent settings per tool */
agentToolConsents: z.record(z.string(), z.string()).nullable(),
/** Experiment flags */
experiments: z.record(z.string(), z.boolean()).nullable(),
/** Custom node path override */
customNodePath: z.string().nullable(),
/** Map of provider ID -> whether configured (has API key). No secrets. */
providerSetupStatus: z.record(z.string(), z.boolean()),
});
// -- App metadata --
const DebugAppInfoSchema = z.object({
id: z.number(),
title: z.string(),
messages: z.array(
z.object({
name: z.string(),
/** Relative app path (not full filesystem path) */
path: z.string(),
createdAt: z.string(),
updatedAt: z.string(),
// Integration identifiers (non-secret)
githubOrg: z.string().nullable(),
githubRepo: z.string().nullable(),
githubBranch: z.string().nullable(),
supabaseProjectId: z.string().nullable(),
supabaseOrganizationSlug: z.string().nullable(),
neonProjectId: z.string().nullable(),
vercelProjectId: z.string().nullable(),
vercelProjectName: z.string().nullable(),
vercelDeploymentUrl: z.string().nullable(),
// Dev commands
installCommand: z.string().nullable(),
startCommand: z.string().nullable(),
// Chat context configuration
chatContext: z.any().nullable(),
// Theme
themeId: z.string().nullable(),
});
// -- Message with full debug detail --
const DebugMessageSchema = z.object({
id: z.number(),
role: z.string(),
role: z.enum(["user", "assistant"]),
/** Human-readable message text */
content: z.string(),
approvalState: z.string().nullable().optional(),
/** ISO 8601 timestamp */
createdAt: z.string(),
/**
* Full AI SDK structured message data (tool calls, image refs, multi-turn state).
* Base64 image data is stripped and replaced with:
* { type: "image", image: "[stripped]", mediaType: "...", _strippedByteLength: N }
*/
aiMessagesJson: z.any().nullable(),
/** Model name used to generate this response (assistant messages only) */
model: z.string().nullable(),
/** Total tokens used for this response (assistant messages only) */
totalTokens: z.number().nullable(),
/** Approval state: "approved" | "rejected" | null */
approvalState: z.enum(["approved", "rejected"]).nullable(),
/** Git commit hash of codebase when this message was created */
sourceCommitHash: z.string().nullable(),
/** Git commit hash of codebase after changes from this message were applied */
commitHash: z.string().nullable(),
/** Pro request UUID for billing/tracking */
requestId: z.string().nullable(),
/** Whether this message used the free agent mode quota */
usingFreeAgentModeQuota: z.boolean().nullable(),
});
// -- Chat with messages --
const DebugChatSchema = z.object({
id: z.number(),
appId: z.number(),
title: z.string().nullable(),
/** Git commit hash at start of this chat */
initialCommitHash: z.string().nullable(),
/** ISO 8601 timestamp */
createdAt: z.string(),
messages: z.array(DebugMessageSchema),
});
// -- Provider / model configuration (no secrets) --
const DebugProvidersSchema = z.object({
/** Custom provider definitions from language_model_providers table */
customProviders: z.array(
z.object({
id: z.string(),
name: z.string(),
hasApiBaseUrl: z.boolean(),
envVarName: z.string().nullable(),
}),
),
/** Custom model definitions from language_models table */
customModels: z.array(
z.object({
id: z.number(),
displayName: z.string(),
apiName: z.string(),
builtinProviderId: z.string().nullable(),
customProviderId: z.string().nullable(),
maxOutputTokens: z.number().nullable(),
contextWindow: z.number().nullable(),
}),
),
});
// -- MCP server configuration (no env/header secrets) --
const DebugMcpServerSchema = z.object({
id: z.number(),
name: z.string(),
transport: z.string(),
command: z.string().nullable(),
args: z.array(z.string()).nullable(),
url: z.string().nullable(),
enabled: z.boolean(),
// NOTE: envJson and headersJson are intentionally EXCLUDED (may contain secrets)
});
// -- Top-level bundle --
/**
* Complete session debug bundle for upload.
*
* Contains all non-sensitive data needed to debug a chat session:
* system info, user settings, app config, full chat messages with
* AI SDK JSON, provider/model setup, MCP servers, codebase snapshot,
* and application logs.
*
* Sensitive data (API keys, OAuth tokens, MCP env vars) is stripped.
* Base64 image data in AI SDK messages is replaced with placeholders.
*/
export const SessionDebugBundleSchema = z.object({
/** Schema version number. Bump on breaking changes. */
schemaVersion: z.number(),
/** ISO 8601 timestamp of when this bundle was exported */
exportedAt: z.string(),
/** Runtime environment info */
system: DebugSystemInfoSchema,
/** Non-sensitive user settings snapshot */
settings: DebugSettingsSchema,
/** App configuration and integration metadata */
app: DebugAppInfoSchema,
/** Chat with full message history including AI SDK JSON */
chat: DebugChatSchema,
/** Custom provider and model definitions (no secrets) */
providers: DebugProvidersSchema,
/** MCP server configurations (no env/header secrets) */
mcpServers: z.array(DebugMcpServerSchema),
/** Formatted codebase snapshot */
codebase: z.string(),
/** Application logs (last 1000 lines) */
logs: z.string(),
});
export type ChatLogsData = z.infer<typeof ChatLogsDataSchema>;
export type SessionDebugBundle = z.infer<typeof SessionDebugBundleSchema>;
// =============================================================================
// Deep Link Schemas
......@@ -133,11 +335,11 @@ export const miscContracts = {
output: z.void(),
}),
// Chat logs
getChatLogs: defineContract({
channel: "get-chat-logs",
// Session debug bundle
getSessionDebugBundle: defineContract({
channel: "get-session-debug-bundle",
input: z.number(), // chatId
output: ChatLogsDataSchema,
output: SessionDebugBundleSchema,
}),
// Console logs
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论