import { createIcons, Bot, CirclePlus, FileText, FolderGit2, GitBranch, Link, List, ListChecks, RefreshCw, Send, X } from "lucide"; import "./styles.css"; import { appStore, CHROME_PROFILE_NAME, findKnownUrlForBase, getAllWorkspaces, ISSUE_LABEL_OPTIONS, type AgentProfile, type AddUrlMode, type AppView, type IssueLabelOption, type OpenIssue, type OpenIssuesStatusFilter, type ScreenshotAttachment, type WorkspacePrompt } from "./store"; import type { GiteaIssueAttachment, GiteaIssueComment, GiteaIssueSummary, GiteaRepoSummary } from "./api"; import type { KnownUrl, LocalContext, LocalFolderCandidate, LocalWorkspace } from "./localContext"; type CropHandle = "n" | "e" | "s" | "w" | "nw" | "ne" | "sw" | "se"; const status = document.querySelector("#status"); const refreshData = document.querySelector("#refresh-data"); const addIssueButton = document.querySelector("#add-issue"); const manageViewSelect = document.querySelector("#manage-view-select"); const viewHeader = document.querySelector("#view-header"); const viewContent = document.querySelector("#view-content"); const viewButtons = Array.from(document.querySelectorAll("[data-view]")); const lucideIcons = { Bot, CirclePlus, FileText, FolderGit2, GitBranch, Link, List, ListChecks, RefreshCw, Send, X }; function hydrateIcons(): void { createIcons({ icons: lucideIcons }); } hydrateIcons(); function escapeHtml(value: string): string { return value.replace(/[&<>"']/g, (char) => { const replacements: Record = { "&": "&", "<": "<", ">": ">", "\"": """, "'": "'" }; return replacements[char]; }); } function formatDate(value: string, emptyText = "No date"): string { if (!value) { return emptyText; } return new Intl.DateTimeFormat(undefined, { month: "short", day: "numeric", hour: "numeric", minute: "2-digit" }).format(new Date(value)); } function formatSha(value: string): string { return value ? value.slice(0, 8) : "unknown"; } function formatBytes(value: number): string { if (!value) { return ""; } if (value < 1024) { return `${value} B`; } if (value < 1024 * 1024) { return `${Math.round(value / 102.4) / 10} KB`; } return `${Math.round(value / 1024 / 102.4) / 10} MB`; } function allUrls(): KnownUrl[] { const state = appStore.getState(); const deletedBases = new Set(state.deletedUrlBases); const urls = [...(state.localContext?.knownUrls ?? []), ...state.userUrls].filter( (knownUrl) => !deletedBases.has(baseUrlFor(knownUrl.url)) ); return urls.filter( (knownUrl, index) => urls.findIndex((item) => baseUrlFor(item.url) === baseUrlFor(knownUrl.url)) === index ); } function baseUrlFor(value: string): string { try { const url = new URL(value); if (url.protocol !== "http:" && url.protocol !== "https:") { return ""; } return `${url.origin}/`; } catch { return ""; } } function isActiveBaseUrlKnown(): boolean { return Boolean(findKnownUrlForBase(appStore.getState())); } function getActiveKnownUrl(): KnownUrl | null { return findKnownUrlForBase(appStore.getState()); } function canAddIssue(): boolean { return Boolean(getActiveKnownUrl()?.giteaRepo); } function getRepoWorkspaceLink(repo: GiteaRepoSummary): string { const context = appStore.getState().localContext; const worktree = context?.gitWorktrees.find((item) => item.giteaRepo?.fullName === repo.fullName); if (!worktree) { return `No workspace link`; } return `./${escapeHtml(worktree.relativePath)}`; } function renderRepos(repos: GiteaRepoSummary[]): string { if (!repos.length) { return `
No Gitea repos loaded yet.
`; } return `
${repos .map( (repo) => `
${escapeHtml(repo.fullName)} ${escapeHtml(repo.lastCommitMessage)}
${formatDate(repo.lastCommitAt, "No commit date")} ${formatSha(repo.lastCommitSha)} ${getRepoWorkspaceLink(repo)}
` ) .join("")}
`; } function renderWorkspaces(workspaces: LocalWorkspace[]): string { const state = appStore.getState(); const userWorkspacePaths = new Set(state.userWorkspaces.map((workspace) => workspace.path)); const workspaceList = workspaces.length ? `
${workspaces .map((workspace) => { const links = workspace.linkedRepos.length ? workspace.linkedRepos .map( (repo) => `${escapeHtml(repo.fullName)}` ) .join("") : `No Gitea repo link established`; return `

${escapeHtml(workspace.name)}

${escapeHtml(workspace.path)}

${ userWorkspacePaths.has(workspace.path) ? `` : `Generated` }
`; }) .join("")}
` : `
No workspaces found.
`; return `
${workspaceList} `; } function renderAgents(agents: AgentProfile[]): string { const state = appStore.getState(); const draft = state.agentDraft; return `
${agents .map( (agent) => `

${escapeHtml(agent.name)}

${escapeHtml(agent.file)} ${escapeHtml(agent.description)}
` ) .join("")}
`; } function workspacePathOptions(workspaces: LocalWorkspace[], selectedPath: string): string { if (!workspaces.length) { return selectedPath ? `` : ""; } return workspaces .map( (workspace) => `` ) .join(""); } function renderPrompts(prompts: WorkspacePrompt[]): string { const state = appStore.getState(); const draft = state.promptDraft; return `
${ prompts.length ? prompts .map( (prompt) => `

${escapeHtml(prompt.name)}

${escapeHtml(prompt.workspacePath)}
` ) .join("") : `
No workspace prompts saved yet.
` }
`; } function renderUrls(urls: KnownUrl[]): string { const state = appStore.getState(); const canOpenAddUrl = Boolean(state.activeBaseUrl && !isActiveBaseUrlKnown()); const urlList = urls.length ? `
${urls .map( (knownUrl) => ` ` ) .join("")}
` : `
No known URLs registered.
`; return `
${escapeHtml(state.activeBaseUrl || "Open an http or https tab to add a URL.")}
${urlList} `; } function selectedAttribute(value: string, selectedValue: string): string { return value === selectedValue ? " selected" : ""; } function workspaceOptions(workspaces: LocalWorkspace[], selectedValue: string): string { return workspaces .map( (workspace) => `` ) .join(""); } function repoOptions(context: LocalContext, selectedValue: string): string { const repos = context.gitWorktrees .map((worktree) => worktree.giteaRepo) .filter((repo): repo is NonNullable => Boolean(repo)) .filter((repo, index, repos) => repos.findIndex((item) => item.fullName === repo.fullName) === index) .sort((a, b) => a.fullName.localeCompare(b.fullName)); return repos .map( (repo) => `` ) .join(""); } function renderAddUrl(): string { const state = appStore.getState(); const context = state.localContext; if (!state.activeBaseUrl) { return `
Open an http or https tab before adding a URL link.
`; } if (isActiveBaseUrlKnown()) { return `
${escapeHtml(state.activeBaseUrl)} is already linked.
`; } if (!context) { return `
Workspace context has not loaded yet.
`; } const mode = state.addUrlDraft.mode; const selectedValue = state.addUrlDraft.selectedValue; const workspaces = getAllWorkspaces(state); return `
Link by
`; } function workspaceFolderCandidates(): LocalFolderCandidate[] { const state = appStore.getState(); const workspaceCandidates = getAllWorkspaces(state).map((workspace) => ({ name: workspace.name, path: workspace.path, source: "workspace-root" as const })); const contextCandidates = state.localContext?.folderCandidates ?? []; const candidates = [...workspaceCandidates, ...contextCandidates]; return candidates .filter((candidate, index) => candidates.findIndex((item) => item.path === candidate.path) === index) .sort((a, b) => a.path.localeCompare(b.path)); } function renderWorkspaceSearchResults(candidates: LocalFolderCandidate[], query: string): string { const normalizedQuery = query.trim().toLowerCase(); const matches = (normalizedQuery ? candidates.filter( (candidate) => candidate.name.toLowerCase().includes(normalizedQuery) || candidate.path.toLowerCase().includes(normalizedQuery) ) : candidates ).slice(0, 40); if (!matches.length) { return `
No matching folders found in the generated folder index.
`; } return `
${matches .map( (candidate) => ` ` ) .join("")}
`; } function renderWorkspaceDialog(): string { const state = appStore.getState(); const draft = state.workspaceDraft; return `
${renderWorkspaceSearchResults(workspaceFolderCandidates(), draft.search)}
`; } function renderActiveDialog(): string { const state = appStore.getState(); if (!state.activeDialog) { return ""; } const title = state.activeDialog === "add-url" ? "Add URL" : "Add workspace"; const body = state.activeDialog === "add-url" ? renderAddUrl() : renderWorkspaceDialog(); return ` `; } function renderFileList(files: File[], target: "issue" | "comment" | "issue-attachment"): string { if (!files.length) { return `No files selected.`; } const attribute = target === "issue" ? "data-delete-issue-file" : target === "comment" ? "data-delete-comment-file" : "data-delete-issue-asset-file"; return `
${files .map( (file, index) => `
${escapeHtml(file.name)} ${escapeHtml(formatBytes(file.size))}
` ) .join("")}
`; } function renderAgentOptions(agents: AgentProfile[], selectedAgentId: string): string { return agents .map( (agent) => `` ) .join(""); } function renderIssuePromptOptions(prompts: WorkspacePrompt[], selectedPromptId: string): string { return ` ${prompts .map( (prompt) => `` ) .join("")} `; } function renderLabelOptions(selectedLabels: IssueLabelOption[], target: "draft" | "edit"): string { return `
Labels
${ISSUE_LABEL_OPTIONS.map( (label) => ` ` ).join("")}
`; } function renderIssueLabelFilter(selectedLabel: IssueLabelOption | ""): string { return ` `; } function renderIssueStatusFilters(selectedStatuses: OpenIssuesStatusFilter[]): string { return `
Status
`; } function renderBodyText(body: string, emptyText: string): string { const trimmed = body.trim(); if (!trimmed) { return `

${escapeHtml(emptyText)}

`; } return `
${escapeHtml(trimmed)}
`; } function renderIssueAttachments(attachments: GiteaIssueAttachment[]): string { if (!attachments.length) { return `

No issue attachments.

`; } const state = appStore.getState(); return `
    ${attachments .map( (attachment) => `
  • ${escapeHtml(attachment.name)} ${attachment.size ? `${escapeHtml(formatBytes(attachment.size))}` : ""}
  • ` ) .join("")}
`; } function renderIssueComments(comments: GiteaIssueComment[]): string { if (!comments.length) { return `

No comments yet.

`; } return `
${comments .map( (comment) => ` ` ) .join("")}
`; } function renderScreenshotList( screenshots: ScreenshotAttachment[], deleteAction: "issue" | "comment" | "issue-attachment" ): string { if (!screenshots.length) { return `No screenshots captured.`; } const attribute = deleteAction === "issue" ? "data-delete-screenshot" : deleteAction === "comment" ? "data-delete-comment-screenshot" : "data-delete-issue-asset-screenshot"; const previewTarget = deleteAction; return `
${screenshots .map( (screenshot) => `
${escapeHtml(screenshot.name)}
` ) .join("")}
`; } function getPreviewScreenshot(): ScreenshotAttachment | null { const state = appStore.getState(); const preview = state.screenshotPreview; if (!preview) { return null; } const screenshots = preview.target === "issue" ? state.issueDraft.screenshots : preview.target === "comment" ? state.issueCommentDraft.screenshots : state.issueAttachmentDraft.screenshots; return screenshots.find((screenshot) => screenshot.id === preview.id) ?? null; } function renderScreenshotPreview(): string { const state = appStore.getState(); const preview = state.screenshotPreview; const screenshot = getPreviewScreenshot(); if (!preview || !screenshot) { return ""; } const crop = preview.crop; return ` `; } function renderIssueRows(issues: OpenIssue[], selectedIssueNumber: number | null, selectedRepoFullName: string): string { const state = appStore.getState(); if (!issues.length) { return `
No issues found.
`; } return `
${issues .map( (issue) => `
` ) .join("")}
`; } function renderIssueCommentForm(issue: GiteaIssueSummary): string { const state = appStore.getState(); const draft = state.issueCommentDraft; const canSubmit = Boolean(draft.content.trim() || draft.screenshots.length || draft.files.length) && !state.isCreatingIssueComment; return `
#${issue.number} ${escapeHtml(issue.title)} ${escapeHtml(issue.state)}
Screenshots
${renderScreenshotList(draft.screenshots, "comment")}
${renderFileList(draft.files, "comment")}
`; } function renderIssueAttachmentForm(): string { const state = appStore.getState(); const draft = state.issueAttachmentDraft; const canUpload = Boolean(draft.screenshots.length || draft.files.length) && !state.isUploadingIssueAttachments; return `
Add attachments
${renderScreenshotList(draft.screenshots, "issue-attachment")} ${renderFileList(draft.files, "issue-attachment")}
`; } function renderSelectedIssueDetail(): string { const state = appStore.getState(); const detail = state.selectedIssueDetail; if (state.isLoadingIssueDetail && !detail) { return `
Loading issue details...
`; } if (!detail) { return `
Issue details are not loaded yet.
`; } const draft = state.selectedIssueEditDraft; const detailLabels = ISSUE_LABEL_OPTIONS.filter((label) => detail.issue.labels.some((issueLabel) => issueLabel.toLowerCase() === label.toLowerCase()) ); const hasChanges = draft.subject !== detail.issue.title || draft.content !== detail.issue.body || draft.labels.join("|") !== detailLabels.join("|"); return `
${renderLabelOptions(draft.labels, "edit")}
Attachments
${renderIssueAttachments(detail.attachments)} ${renderIssueAttachmentForm()}
Comments
${renderIssueComments(detail.comments)}
`; } function renderOpenIssues(): string { const state = appStore.getState(); return `
${renderIssueStatusFilters(state.openIssuesStatusFilters)} ${renderIssueLabelFilter(state.openIssuesLabelFilter)}
${ state.openIssuesLimitToCurrentRepo && !getActiveKnownUrl()?.giteaRepo ? `
Open a known URL linked to a Gitea repo, or turn off the current repo filter.
` : state.isLoadingTargetIssues && !state.openIssues.length ? `
Loading issues...
` : renderIssueRows(state.openIssues, state.selectedIssueNumber, state.selectedIssueRepoFullName) }
${ state.selectedIssueNumber ? `${renderSelectedIssueDetail()}${ state.openIssues.find( (issue) => issue.number === state.selectedIssueNumber && issue.repoFullName === state.selectedIssueRepoFullName ) ? renderIssueCommentForm( state.openIssues.find( (issue) => issue.number === state.selectedIssueNumber && issue.repoFullName === state.selectedIssueRepoFullName ) as OpenIssue ) : "" }` : "" } `; } function renderAddIssue(): string { const state = appStore.getState(); const knownUrl = getActiveKnownUrl(); const draft = state.issueDraft; if (!knownUrl) { return `
Open a known URL before creating an issue.
`; } if (!knownUrl.giteaRepo) { return `
${escapeHtml(knownUrl.label)} is linked to a workspace but not a Gitea repo.
`; } const workspacePrompts = state.prompts.filter((prompt) => prompt.workspacePath === knownUrl.workspacePath); return `
Target repo ${escapeHtml(knownUrl.giteaRepo.fullName)} Active URL ${escapeHtml(state.activeTabUrl || "Unknown active tab")} Known URL ${escapeHtml(knownUrl.url)} ${escapeHtml(knownUrl.workspacePath)} Chrome profile: ${escapeHtml(CHROME_PROFILE_NAME)}
${renderLabelOptions(draft.labels, "draft")}
Screenshots
${renderScreenshotList(draft.screenshots, "issue")}
${renderFileList(draft.files, "issue")}
`; } function openUrl(url: string): void { if (globalThis.chrome?.tabs?.create) { void globalThis.chrome.tabs.create({ url }); return; } window.open(url, "_blank", "noopener,noreferrer"); } function bindWorkspaceFolderResultActions(): void { viewContent?.querySelectorAll("[data-select-workspace-folder]").forEach((button) => { button.addEventListener("click", () => { const path = button.dataset.selectWorkspaceFolder; const name = button.dataset.folderName; if (path) { appStore.getState().setWorkspaceDraft({ path, name: appStore.getState().workspaceDraft.name || name || "" }); } }); }); } function bindViewActions(): void { viewContent?.querySelector("#open-add-url-dialog")?.addEventListener("click", () => { appStore.getState().openDialog("add-url"); }); viewContent?.querySelector("#open-workspace-dialog")?.addEventListener("click", () => { appStore.getState().openDialog("add-workspace"); }); viewContent?.querySelector("[data-close-app-dialog]")?.addEventListener("click", () => { appStore.getState().closeDialog(); }); viewContent?.querySelector("#agent-name")?.addEventListener("input", (event) => { appStore.getState().setAgentDraft({ name: (event.target as HTMLInputElement).value }); }); viewContent?.querySelector("#agent-file")?.addEventListener("input", (event) => { appStore.getState().setAgentDraft({ file: (event.target as HTMLInputElement).value }); }); viewContent?.querySelector("#agent-description")?.addEventListener("input", (event) => { appStore.getState().setAgentDraft({ description: (event.target as HTMLInputElement).value }); }); viewContent?.querySelector("#agent-content")?.addEventListener("input", (event) => { appStore.getState().setAgentDraft({ content: (event.target as HTMLTextAreaElement).value }); }); viewContent?.querySelector("#new-agent")?.addEventListener("click", () => { appStore.getState().startNewAgent(); }); viewContent?.querySelector("#save-agent")?.addEventListener("click", () => { void appStore.getState().saveAgent(); }); viewContent?.querySelectorAll("[data-edit-agent]").forEach((button) => { button.addEventListener("click", () => { const id = button.dataset.editAgent; if (id) { appStore.getState().editAgent(id); } }); }); viewContent?.querySelectorAll("[data-delete-agent]").forEach((button) => { button.addEventListener("click", () => { const id = button.dataset.deleteAgent; if (id && window.confirm("Delete this agent?")) { void appStore.getState().deleteAgent(id); } }); }); viewContent?.querySelector("#workspace-name")?.addEventListener("input", (event) => { appStore.getState().setWorkspaceDraft({ name: (event.target as HTMLInputElement).value }); }); viewContent?.querySelector("#workspace-search")?.addEventListener("input", (event) => { appStore.getState().setWorkspaceDraft({ search: (event.target as HTMLInputElement).value }); }); bindWorkspaceFolderResultActions(); viewContent?.querySelector("#save-workspace")?.addEventListener("click", () => { void appStore.getState().saveWorkspace(); }); viewContent?.querySelectorAll("[data-delete-user-workspace]").forEach((button) => { button.addEventListener("click", () => { const path = button.dataset.deleteUserWorkspace; if (path && window.confirm("Delete this user-added workspace?")) { void appStore.getState().deleteUserWorkspace(path); } }); }); viewContent?.querySelector("#prompt-name")?.addEventListener("input", (event) => { appStore.getState().setPromptDraft({ name: (event.target as HTMLInputElement).value }); }); viewContent?.querySelector("#prompt-workspace")?.addEventListener("change", (event) => { appStore.getState().setPromptDraft({ workspacePath: (event.target as HTMLSelectElement).value }); }); viewContent?.querySelector("#prompt-content")?.addEventListener("input", (event) => { appStore.getState().setPromptDraft({ content: (event.target as HTMLTextAreaElement).value }); }); viewContent?.querySelector("#new-prompt")?.addEventListener("click", () => { appStore.getState().startNewPrompt(); }); viewContent?.querySelector("#save-prompt")?.addEventListener("click", () => { void appStore.getState().savePrompt(); }); viewContent?.querySelectorAll("[data-edit-prompt]").forEach((button) => { button.addEventListener("click", () => { const id = button.dataset.editPrompt; if (id) { appStore.getState().editPrompt(id); } }); }); viewContent?.querySelectorAll("[data-delete-prompt]").forEach((button) => { button.addEventListener("click", () => { const id = button.dataset.deletePrompt; if (id && window.confirm("Delete this prompt?")) { void appStore.getState().deletePrompt(id); } }); }); viewContent?.querySelectorAll("[data-open-url]").forEach((button) => { button.addEventListener("click", () => { const url = button.dataset.openUrl; if (url) { openUrl(url); } }); }); viewContent?.querySelectorAll("[data-delete-url]").forEach((button) => { button.addEventListener("click", () => { const url = button.dataset.deleteUrl; if (url) { void appStore.getState().deleteUrlLink(url); } }); }); viewContent?.querySelectorAll("input[name='add-url-mode']").forEach((input) => { input.addEventListener("change", () => { appStore.getState().setAddUrlDraft({ mode: input.value as AddUrlMode, selectedValue: "" }); }); }); viewContent?.querySelector("#add-url-selection")?.addEventListener("change", (event) => { appStore.getState().setAddUrlDraft({ selectedValue: (event.target as HTMLSelectElement).value }); }); viewContent?.querySelector("#save-url-link")?.addEventListener("click", () => { void appStore.getState().saveCurrentUrlLink(); }); viewContent?.querySelector("#limit-open-issues")?.addEventListener("change", (event) => { appStore.getState().setOpenIssuesLimit((event.target as HTMLInputElement).checked); }); viewContent?.querySelectorAll("[data-issues-status-filter]").forEach((input) => { input.addEventListener("change", () => { const statusFilters = Array.from( viewContent.querySelectorAll("[data-issues-status-filter]:checked") ).map((checkedInput) => checkedInput.value as OpenIssuesStatusFilter); appStore.getState().setOpenIssuesStatusFilters(statusFilters); }); }); viewContent?.querySelector("#issues-label-filter")?.addEventListener("change", (event) => { appStore.getState().setOpenIssuesLabelFilter((event.target as HTMLSelectElement).value as IssueLabelOption | ""); }); viewContent?.querySelector("#issue-subject")?.addEventListener("input", (event) => { appStore.getState().setIssueDraft({ subject: (event.target as HTMLInputElement).value }); }); viewContent?.querySelector("#issue-content")?.addEventListener("input", (event) => { appStore.getState().setIssueDraft({ content: (event.target as HTMLTextAreaElement).value }); }); viewContent?.querySelector("#issue-prompt")?.addEventListener("change", (event) => { appStore.getState().setIssuePrompt((event.target as HTMLSelectElement).value); }); viewContent?.querySelector("#issue-agent")?.addEventListener("change", (event) => { appStore.getState().setIssueDraft({ agentId: (event.target as HTMLSelectElement).value }); }); viewContent?.querySelector("#issue-files")?.addEventListener("change", (event) => { const input = event.target as HTMLInputElement; appStore.getState().setIssueFiles(Array.from(input.files ?? [])); input.value = ""; }); viewContent?.querySelector("#capture-screenshot")?.addEventListener("click", () => { void appStore.getState().captureScreenshot(); }); viewContent?.querySelectorAll("[data-delete-screenshot]").forEach((button) => { button.addEventListener("click", () => { const id = button.dataset.deleteScreenshot; if (id) { appStore.getState().deleteIssueScreenshot(id); } }); }); viewContent?.querySelectorAll("[data-delete-issue-file]").forEach((button) => { button.addEventListener("click", () => { const index = Number(button.dataset.deleteIssueFile); if (Number.isInteger(index)) { appStore.getState().deleteIssueFile(index); } }); }); viewContent?.querySelectorAll("[data-preview-screenshot]").forEach((button) => { button.addEventListener("click", () => { const id = button.dataset.previewScreenshot; const target = button.dataset.previewTarget; if (id && (target === "issue" || target === "comment" || target === "issue-attachment")) { appStore.getState().openScreenshotPreview(target, id); } }); }); viewContent?.querySelectorAll("[data-select-issue]").forEach((button) => { button.addEventListener("click", () => { const issueNumber = Number(button.dataset.selectIssue); const repoFullName = button.dataset.issueRepo; if (Number.isFinite(issueNumber)) { appStore.getState().selectIssue(issueNumber, repoFullName); } }); }); viewContent?.querySelectorAll("[data-delete-issue]").forEach((button) => { button.addEventListener("click", () => { const issueNumber = Number(button.dataset.deleteIssue); const repoFullName = button.dataset.issueRepo; if (Number.isFinite(issueNumber) && window.confirm(`Delete issue #${issueNumber}?`)) { void appStore.getState().deleteIssue(issueNumber, repoFullName); } }); }); viewContent?.querySelector("#refresh-target-issues")?.addEventListener("click", () => { void appStore.getState().loadOpenIssues(true); }); viewContent?.querySelector("#refresh-issue-detail")?.addEventListener("click", () => { void appStore.getState().loadSelectedIssueDetail(true); }); viewContent?.querySelector("#close-selected-issue")?.addEventListener("click", () => { appStore.getState().clearSelectedIssue(); }); viewContent?.querySelector("#send-selected-issue-to-codex")?.addEventListener("click", () => { const state = appStore.getState(); const selectedIssue = state.openIssues.find( (issue) => issue.number === state.selectedIssueNumber && issue.repoFullName === state.selectedIssueRepoFullName ); console.log("Send to Codex", { issue: state.selectedIssueDetail?.issue ?? selectedIssue ?? null, repoFullName: state.selectedIssueRepoFullName, knownUrl: selectedIssue?.knownUrl ?? null, attachments: state.selectedIssueDetail?.attachments ?? [], comments: state.selectedIssueDetail?.comments ?? [] }); }); viewContent?.querySelector("#selected-issue-subject")?.addEventListener("input", (event) => { appStore.getState().setSelectedIssueEditDraft({ subject: (event.target as HTMLInputElement).value }); }); viewContent?.querySelector("#selected-issue-content")?.addEventListener("input", (event) => { appStore.getState().setSelectedIssueEditDraft({ content: (event.target as HTMLTextAreaElement).value }); }); viewContent?.querySelector("#save-selected-issue")?.addEventListener("click", () => { void appStore.getState().saveSelectedIssueEdits(); }); viewContent?.querySelectorAll("[data-label-target]").forEach((input) => { input.addEventListener("change", () => { const target = input.dataset.labelTarget; const labels = Array.from( viewContent.querySelectorAll(`input[data-label-target='${target}']:checked`) ).map((checkedInput) => checkedInput.value as IssueLabelOption); if (target === "draft") { appStore.getState().setIssueDraft({ labels }); } else if (target === "edit") { appStore.getState().setSelectedIssueEditDraft({ labels }); } }); }); viewContent?.querySelectorAll("[data-delete-issue-attachment]").forEach((button) => { button.addEventListener("click", () => { const attachmentId = Number(button.dataset.deleteIssueAttachment); if (Number.isFinite(attachmentId) && window.confirm("Delete this attachment from the issue?")) { void appStore.getState().deleteSelectedIssueAttachment(attachmentId); } }); }); viewContent?.querySelector("#issue-asset-files")?.addEventListener("change", (event) => { const input = event.target as HTMLInputElement; appStore.getState().setIssueAttachmentFiles(Array.from(input.files ?? [])); input.value = ""; }); viewContent?.querySelector("#capture-issue-asset-screenshot")?.addEventListener("click", () => { void appStore.getState().captureIssueAttachmentScreenshot(); }); viewContent?.querySelectorAll("[data-delete-issue-asset-screenshot]").forEach((button) => { button.addEventListener("click", () => { const id = button.dataset.deleteIssueAssetScreenshot; if (id) { appStore.getState().deleteIssueAttachmentScreenshot(id); } }); }); viewContent?.querySelectorAll("[data-delete-issue-asset-file]").forEach((button) => { button.addEventListener("click", () => { const index = Number(button.dataset.deleteIssueAssetFile); if (Number.isInteger(index)) { appStore.getState().deleteIssueAttachmentFile(index); } }); }); viewContent?.querySelector("#upload-issue-assets")?.addEventListener("click", () => { void appStore.getState().uploadSelectedIssueAttachments(); }); viewContent?.querySelector("#issue-comment-content")?.addEventListener("input", (event) => { appStore.getState().setIssueCommentDraft({ content: (event.target as HTMLTextAreaElement).value }); }); viewContent?.querySelector("#issue-comment-files")?.addEventListener("change", (event) => { const input = event.target as HTMLInputElement; appStore.getState().setIssueCommentFiles(Array.from(input.files ?? [])); input.value = ""; }); viewContent?.querySelector("#capture-comment-screenshot")?.addEventListener("click", () => { void appStore.getState().captureIssueCommentScreenshot(); }); viewContent?.querySelectorAll("[data-delete-comment-screenshot]").forEach((button) => { button.addEventListener("click", () => { const id = button.dataset.deleteCommentScreenshot; if (id) { appStore.getState().deleteIssueCommentScreenshot(id); } }); }); viewContent?.querySelectorAll("[data-delete-comment-file]").forEach((button) => { button.addEventListener("click", () => { const index = Number(button.dataset.deleteCommentFile); if (Number.isInteger(index)) { appStore.getState().deleteIssueCommentFile(index); } }); }); bindCropInteractions(); viewContent?.querySelector("[data-close-screenshot-preview]")?.addEventListener("click", () => { appStore.getState().closeScreenshotPreview(); }); viewContent?.querySelector("[data-remove-preview-screenshot]")?.addEventListener("click", () => { appStore.getState().removePreviewScreenshot(); }); viewContent?.querySelector("[data-apply-screenshot-crop]")?.addEventListener("click", () => { void appStore.getState().applyScreenshotCrop(); }); viewContent?.querySelector("#submit-issue-comment")?.addEventListener("click", () => { void appStore.getState().createIssueComment(); }); viewContent?.querySelector("#create-issue")?.addEventListener("click", () => { void appStore.getState().createIssue(); }); } function bindCropInteractions(): void { const frame = viewContent?.querySelector(".crop-frame"); const cropBox = viewContent?.querySelector("[data-crop-drag]"); if (!frame || !cropBox) { return; } cropBox.addEventListener("pointerdown", (event) => { const state = appStore.getState(); const preview = state.screenshotPreview; if (!preview) { return; } event.preventDefault(); cropBox.setPointerCapture(event.pointerId); const handle = (event.target as HTMLElement).dataset.cropHandle as CropHandle | undefined; const frameRect = frame.getBoundingClientRect(); const startX = event.clientX; const startY = event.clientY; const startCrop = { ...preview.crop }; const updateCrop = (moveEvent: PointerEvent): void => { const deltaX = ((moveEvent.clientX - startX) / frameRect.width) * 100; const deltaY = ((moveEvent.clientY - startY) / frameRect.height) * 100; if (!handle) { appStore.getState().setScreenshotPreviewCrop({ left: startCrop.left + deltaX, top: startCrop.top + deltaY }); return; } const nextCrop = { ...startCrop }; if (handle.includes("e")) { nextCrop.width = startCrop.width + deltaX; } if (handle.includes("s")) { nextCrop.height = startCrop.height + deltaY; } if (handle.includes("w")) { nextCrop.left = startCrop.left + deltaX; nextCrop.width = startCrop.width - deltaX; } if (handle.includes("n")) { nextCrop.top = startCrop.top + deltaY; nextCrop.height = startCrop.height - deltaY; } appStore.getState().setScreenshotPreviewCrop(nextCrop); }; const stopCrop = (): void => { window.removeEventListener("pointermove", updateCrop); window.removeEventListener("pointerup", stopCrop); window.removeEventListener("pointercancel", stopCrop); }; window.addEventListener("pointermove", updateCrop); window.addEventListener("pointerup", stopCrop); window.addEventListener("pointercancel", stopCrop); }); } function renderView(): void { if (!viewHeader || !viewContent) { return; } const state = appStore.getState(); const context = state.localContext; const titles: Record = { "add-issue": "Add issue", "add-url": "Add URL", agents: "Agents", "open-issues": "Issues", prompts: "Prompts", urls: "URLs", repos: "Gitea repos", workspaces: "Workspaces" }; const descriptions: Record = { "add-issue": "Create a Gitea issue for the repo linked to the active tab.", "add-url": "Link the active tab base URL to a workspace or Gitea repo.", agents: "Agent files available for issue handoff.", "open-issues": "Recent issues ordered by creation time.", prompts: "Workspace-scoped prompt templates for issue content.", urls: "Known browser targets and their workspace/repo links.", repos: "Repositories ordered by latest default-branch commit.", workspaces: `Authoritative workspaces from ${context?.workspaceRoot || "workspace_Father"}.` }; viewHeader.hidden = state.activeView === "add-issue"; viewHeader.innerHTML = state.activeView === "add-issue" ? "" : `

${titles[state.activeView]}

${escapeHtml(descriptions[state.activeView])}

`; if (state.activeView === "repos") { viewContent.innerHTML = renderRepos(state.repos); } else if (state.activeView === "workspaces") { viewContent.innerHTML = renderWorkspaces(getAllWorkspaces(state)); } else if (state.activeView === "prompts") { viewContent.innerHTML = renderPrompts(state.prompts); } else if (state.activeView === "agents") { viewContent.innerHTML = renderAgents(state.agents); } else if (state.activeView === "open-issues") { viewContent.innerHTML = renderOpenIssues(); } else if (state.activeView === "add-issue") { viewContent.innerHTML = renderAddIssue(); } else if (state.activeView === "add-url") { viewContent.innerHTML = renderAddUrl(); } else { viewContent.innerHTML = renderUrls(allUrls()); } viewContent.insertAdjacentHTML("beforeend", renderActiveDialog()); viewContent.insertAdjacentHTML("beforeend", renderScreenshotPreview()); hydrateIcons(); bindViewActions(); } function renderChrome(): void { const state = appStore.getState(); if (status) { status.textContent = state.statusMessage; status.dataset.tone = state.statusTone; } if (refreshData) { refreshData.disabled = state.isLoadingData; } if (addIssueButton) { addIssueButton.disabled = !canAddIssue(); } if (manageViewSelect) { manageViewSelect.value = ["agents", "prompts", "workspaces"].includes(state.activeView) ? state.activeView : ""; } viewButtons.forEach((button) => { const view = button.dataset.view; button.dataset.active = view === state.activeView ? "true" : "false"; }); } function shouldRenderView(): boolean { const activeElement = document.activeElement; if (!activeElement || !viewContent?.contains(activeElement)) { return true; } return !activeElement.matches( "#issue-subject, #issue-content, #issue-agent, #selected-issue-subject, #selected-issue-content, #issue-comment-content, #agent-name, #agent-file, #agent-description, #agent-content, #prompt-name, #prompt-content, #workspace-name, #workspace-search, [data-label-target]" ); } function syncFocusedFormControls(): void { const state = appStore.getState(); const createIssue = viewContent?.querySelector("#create-issue"); const saveSelectedIssue = viewContent?.querySelector("#save-selected-issue"); const submitComment = viewContent?.querySelector("#submit-issue-comment"); const saveWorkspace = viewContent?.querySelector("#save-workspace"); const workspaceResults = viewContent?.querySelector("#workspace-folder-results"); const workspacePath = viewContent?.querySelector("#workspace-path"); const workspaceName = viewContent?.querySelector("#workspace-name"); if (createIssue) { createIssue.disabled = !state.issueDraft.subject.trim() || state.isCreatingIssue; } if (submitComment) { submitComment.disabled = (!state.issueCommentDraft.content.trim() && !state.issueCommentDraft.screenshots.length && !state.issueCommentDraft.files.length) || state.isCreatingIssueComment; } if (saveWorkspace) { saveWorkspace.disabled = !state.workspaceDraft.path.trim(); } if (workspaceResults) { workspaceResults.innerHTML = renderWorkspaceSearchResults(workspaceFolderCandidates(), state.workspaceDraft.search); bindWorkspaceFolderResultActions(); } if (workspacePath) { workspacePath.value = state.workspaceDraft.path; } if (workspaceName && document.activeElement !== workspaceName) { workspaceName.value = state.workspaceDraft.name; } if (saveSelectedIssue && state.selectedIssueDetail) { const detailLabels = ISSUE_LABEL_OPTIONS.filter((label) => state.selectedIssueDetail?.issue.labels.some((issueLabel) => issueLabel.toLowerCase() === label.toLowerCase()) ); saveSelectedIssue.disabled = !state.selectedIssueEditDraft.subject.trim() || state.isUpdatingIssue || (state.selectedIssueEditDraft.subject === state.selectedIssueDetail.issue.title && state.selectedIssueEditDraft.content === state.selectedIssueDetail.issue.body && state.selectedIssueEditDraft.labels.join("|") === detailLabels.join("|")); } } function render(): void { renderChrome(); if (shouldRenderView()) { renderView(); } else { syncFocusedFormControls(); } ensureOpenIssuesLoaded(); ensureSelectedIssueDetailLoaded(); } function ensureOpenIssuesLoaded(): void { const state = appStore.getState(); const scopeKey = state.openIssuesLimitToCurrentRepo ? `current:${getActiveKnownUrl()?.giteaRepo?.fullName ?? "none"}:${state.openIssuesStatusFilters.join(",")}:${state.openIssuesLabelFilter || "all-labels"}` : `all-repos:${state.openIssuesStatusFilters.join(",")}:${state.openIssuesLabelFilter || "all-labels"}`; if ( state.activeView === "open-issues" && state.openIssuesScopeKey !== scopeKey && !state.isLoadingTargetIssues ) { void appStore.getState().loadOpenIssues(); } } function ensureSelectedIssueDetailLoaded(): void { const state = appStore.getState(); if ( state.activeView === "open-issues" && state.selectedIssueNumber && state.selectedIssueDetailNumber !== state.selectedIssueNumber && !state.isLoadingIssueDetail ) { void appStore.getState().loadSelectedIssueDetail(); } } viewButtons.forEach((button) => { button.addEventListener("click", () => { const view = button.dataset.view as AppView | undefined; if (view) { appStore.getState().setActiveView(view); } }); }); manageViewSelect?.addEventListener("change", () => { const view = manageViewSelect.value as AppView; if (view === "agents" || view === "prompts" || view === "workspaces") { appStore.getState().setActiveView(view); } }); refreshData?.addEventListener("click", () => { void appStore.getState().loadData(); }); function setupActiveTabTracking(): void { let refreshTimer: number | undefined; const scheduleRefresh = (): void => { if (refreshTimer !== undefined) { window.clearTimeout(refreshTimer); } refreshTimer = window.setTimeout(() => { refreshTimer = undefined; void appStore.getState().refreshActiveTab(); }, 80); }; globalThis.chrome?.tabs?.onActivated?.addListener(scheduleRefresh); globalThis.chrome?.tabs?.onUpdated?.addListener((_tabId, changeInfo) => { if (changeInfo.url || changeInfo.status === "complete") { scheduleRefresh(); } }); globalThis.chrome?.windows?.onFocusChanged?.addListener((windowId) => { if (windowId !== chrome.windows.WINDOW_ID_NONE) { scheduleRefresh(); } }); document.addEventListener("visibilitychange", () => { if (!document.hidden) { scheduleRefresh(); } }); window.addEventListener("focus", scheduleRefresh); } function setupLocalContextAutoRefresh(): void { let isRefreshing = false; window.setInterval(() => { const state = appStore.getState(); const shouldRefresh = !document.hidden && !state.isLoadingData && (state.activeView === "workspaces" || state.activeView === "prompts" || state.activeView === "urls" || state.activeDialog === "add-workspace" || state.activeDialog === "add-url"); if (!shouldRefresh || isRefreshing) { return; } isRefreshing = true; appStore.getState().refreshLocalContext().finally(() => { isRefreshing = false; }); }, 10000); } setupActiveTabTracking(); setupLocalContextAutoRefresh(); appStore.subscribe(render); render(); void appStore.getState().loadData();