diff --git a/docs/agent-mode-and-tools.md b/docs/agent-mode-and-tools.md index ada0b2ba..2f9ce247 100644 --- a/docs/agent-mode-and-tools.md +++ b/docs/agent-mode-and-tools.md @@ -183,6 +183,29 @@ If you trust the agent and don't want to review every file change, enable **Auto --- +## Reporting an Issue + +When something goes wrong in Agent Mode, the **Report an Issue** button under **Settings → Copilot → Advanced → Agent Mode debugging** bundles everything a maintainer needs to diagnose it. + +Clicking it opens a short form where you describe what happened. When you submit, Copilot: + +1. Saves a screenshot of the **Agent Mode chat pane** (just the agent panel, not your whole screen). +2. Saves a recent **Agent Mode log** of the behind-the-scenes messages between Copilot and the agent. This log is captured automatically so a report always has recent activity to include; you can turn it off under **Settings → Copilot → Advanced → Keep an Agent Mode activity log**. +3. Opens the folder containing those files in your file manager. +4. Opens a prefilled GitHub issue in your browser. + +The files are **not uploaded for you** — drag them from the opened folder into the GitHub issue to attach them. + +> **Privacy note:** The Agent Mode log can contain your prompts, note contents, and tool inputs/outputs in plaintext. Review the saved files before sharing them publicly. + +### Including the OpenCode log + +When the **OpenCode** backend is active, the form shows an optional checkbox to include OpenCode's own log. It is **off by default** because OpenCode's log is shared across all of your OpenCode sessions, so it may contain activity from unrelated projects. Turn it on only when the issue involves the OpenCode backend itself. + +This feature is available on desktop only. + +--- + ## Related - [Copilot Plus and Self-Host](copilot-plus-and-self-host.md) — Licensing and memory diff --git a/eslint.config.mjs b/eslint.config.mjs index bc506a83..80af4c7d 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -258,6 +258,8 @@ export default [ "src/utils/binaryPath.ts", "src/utils/nodeToolBinDirs.ts", "src/utils/rendererEventsShim.ts", + "src/utils/issueReport.ts", + "src/utils/opencodeLog.ts", ], rules: { "import/no-nodejs-modules": "off", diff --git a/src/agentMode/index.ts b/src/agentMode/index.ts index 74a297c9..355d9c03 100644 --- a/src/agentMode/index.ts +++ b/src/agentMode/index.ts @@ -68,6 +68,8 @@ export { ModelEnableList } from "./ui/ModelEnableList"; export type { ModelEnableGroup, ModelEnableRow } from "./ui/ModelEnableList"; export { PlanPreviewView, PLAN_PREVIEW_VIEW_TYPE } from "./ui/PlanPreviewView"; export type { PlanPreviewViewState } from "./ui/PlanPreviewView"; +export { ReportIssueModal } from "./ui/ReportIssueModal"; +export type { ReportIssueModalParams } from "./ui/ReportIssueModal"; export { getActiveBackendDescriptor, listBackendDescriptors } from "./backends/registry"; export { frameSink as acpFrameSink, setFrameSinkVaultBasePath } from "./session/debugSink"; export { getManagedSkills, SkillManager, SkillsSettings, useManagedSkills } from "./skills"; diff --git a/src/agentMode/ui/ReportIssueModal.tsx b/src/agentMode/ui/ReportIssueModal.tsx new file mode 100644 index 00000000..f8dab406 --- /dev/null +++ b/src/agentMode/ui/ReportIssueModal.tsx @@ -0,0 +1,273 @@ +import { frameSink } from "@/agentMode/session/debugSink"; +import { Button } from "@/components/ui/button"; +import { Checkbox } from "@/components/ui/checkbox"; +import { Textarea } from "@/components/ui/textarea"; +import { logError, logInfo } from "@/logger"; +import { captureViewScreenshot } from "@/utils/captureViewScreenshot"; +import { isDesktopRuntime } from "@/utils/desktopRuntime"; +import { assembleReportBundle, type ReportEnvInfo } from "@/utils/issueReport"; +import { findLatestOpencodeLog } from "@/utils/opencodeLog"; +import { createPluginRoot } from "@/utils/react/createPluginRoot"; +import { cn } from "@/lib/utils"; +import { getSettings } from "@/settings/model"; +import { AlertTriangle } from "lucide-react"; +import { App, Modal, Notice, apiVersion } from "obsidian"; +import React from "react"; +import { Root } from "react-dom/client"; + +const OPENCODE_BACKEND_ID = "opencode"; + +export interface ReportIssueModalParams { + app: App; + /** + * Resolves the element to screenshot, called after this modal closes and just + * before capture. Lets the caller first dismiss any overlay (e.g. the Settings + * window) and reveal the Agent Mode pane so the shot is the chat surface, not + * the dialog. Returns `null` to skip the screenshot (e.g. no agent pane open). + */ + resolveCaptureTarget: () => Promise | HTMLElement | null; + /** Active backend id — gates the opencode-log option. */ + activeBackend: string; + /** Plugin version for the report's environment block. */ + pluginVersion: string; +} + +interface ElectronShell { + openPath?: (path: string) => Promise; + openExternal?: (url: string) => Promise; + showItemInFolder?: (path: string) => void; +} + +function getElectronShell(): ElectronShell | null { + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const electron = require("electron") as + | { shell?: ElectronShell; remote?: { shell?: ElectronShell } } + | undefined; + return electron?.shell ?? electron?.remote?.shell ?? null; + } catch { + return null; + } +} + +function reportsRootDir(): string | null { + try { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const os = require("node:os") as typeof import("node:os"); + // eslint-disable-next-line @typescript-eslint/no-require-imports + const path = require("node:path") as typeof import("node:path"); + return path.join(os.tmpdir(), "obsidian-copilot", "reports"); + } catch { + return null; + } +} + +/** `YYYYMMDD-HHmmss` in local time, for a sortable, filesystem-safe folder name. */ +function formatTimestamp(date: Date): string { + const pad = (n: number) => String(n).padStart(2, "0"); + return ( + `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(date.getDate())}` + + `-${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}` + ); +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => window.setTimeout(resolve, ms)); +} + +interface ReportIssueContentProps { + showOpencodeOption: boolean; + onSubmit: (note: string, includeOpencodeLog: boolean) => void; + onCancel: () => void; +} + +function ReportIssueContent({ showOpencodeOption, onSubmit, onCancel }: ReportIssueContentProps) { + const [note, setNote] = React.useState(""); + // Opt-in by default: the bundled log is opencode's newest *global* log, which + // may belong to an unrelated CLI/Desktop session, so don't attach it silently. + const [includeOpencodeLog, setIncludeOpencodeLog] = React.useState(false); + + return ( +
+
+ What went wrong? +