diff --git a/AGENTS.md b/AGENTS.md index 90fd46e..cf48d93 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,7 +44,7 @@ Use `.env.local` for local secrets. Keep `.env.local` and any real token-bearing - Runtime API calls are centralized in `src/api.ts`. - Side panel state is centralized in the vanilla Zustand store at `src/store.ts`. - The extension opens as a Chrome side panel. It is not a draggable in-page window. -- The side panel has icon menu sections for Add URL, URLs, Gitea repos, Workspaces, Agents, Open Issues, and Add Issue. +- The side panel has icon menu sections for Add URL, URLs, Gitea repos, Workspaces, Agents, Issues, and Add Issue. - Add URL is enabled only when the active tab's base URL is not already in the URLs list. The user must pick either a workspace or a Gitea repo; the app infers the linked counterpart from local context. - URLs can be deleted from the URLs list. User-added URLs are removed from storage; generated known URLs are hidden through the stored deleted-base list. - The Agents view lists available agent files and must allow adding, editing, and deleting persisted agents from `chrome.storage.local`. At least one agent must remain available. @@ -55,11 +55,12 @@ Use `.env.local` for local secrets. Keep `.env.local` and any real token-bearing - Newly created issues must default to the `Change Request` label unless the user changes the selected labels. - When creating an issue, upload the selected rendered agent file as an issue asset along with screenshots and files. - The old Ready checkbox must not be shown because `Ready` is now one of the selectable labels. -- Open Issues owns issue browsing and follow-up. It must list recent issues ordered by creation time, default to open issues for the active URL's linked repo, provide a checkbox to show issues across all loaded repos, and provide a status dropdown for Open, Closed, or Open + Closed. -- Selecting an Open Issues row must show a direct Open in Gitea link, editable issue subject/content/label fields, issue attachments, and comment chain, followed by direct issue attachment controls plus a comment form that can add text, take additional screenshots, attach files, upload those files as issue assets, and submit a Gitea comment with attachment links. -- Saving selected issue edits must use the selected issue's repo identity from Open Issues, include Gitea's current issue content version when available, verify the saved subject/content with a readback before reporting success, and must never fall back to the active tab's repo. +- Issues owns issue browsing and follow-up. It must list recent issues ordered by creation time, default to open issues for the active URL's linked repo, provide a checkbox to show issues across all loaded repos, provide Open and Closed status checkboxes as a multiselect, and provide an empty-by-default label filter. +- Selecting an Issues row must show a direct Open in Gitea link, a Send to Codex button that currently logs the selected issue payload, editable issue subject/content/label fields, issue attachments, and comment chain, followed by direct issue attachment controls plus a comment form that can add text, take additional screenshots, attach files, upload those files as issue assets, and submit a Gitea comment with attachment links. +- Clicking the selected Issues row again or clicking the detail close control must close the selected issue detail panel without deleting or changing the issue. +- Saving selected issue edits must use the selected issue's repo identity from Issues, include Gitea's current issue content version when available, verify the saved subject/content with a readback before reporting success, and must never fall back to the active tab's repo. - Existing issue rows must allow deleting the issue. Existing issue labels must be editable from the selected issue form and saved through the Gitea issue labels endpoint. Existing issue attachments must be removable, and additional screenshots/files must be uploadable directly to the issue without creating a comment. Deletion must require user confirmation. -- Text-entry updates in Add Issue, selected issue edits, Open Issues comments, and Agents must not re-render the focused form control or blur the user while they type. +- Text-entry updates in Add Issue, selected issue edits, Issues comments, and Agents must not re-render the focused form control or blur the user while they type. - Keep the Add Issue view dense; do not show the redundant view header title/description inside this view. - The extension requests `activeTab` and `` host permission so screenshots can be captured from the current Chrome tab even after the side panel has stayed open across tab changes. diff --git a/README.md b/README.md index 889967f..60f726f 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ The deployable extension output is `dist/`. Load `dist/` in Chrome as an unpacke Static assets, including the extension icon assets, live in `public/` and are copied into `dist/` during the build. -The app opens in the Chrome side panel. The top icon menu includes Add URL, URLs, Gitea repos, Workspaces, Agents, Open Issues, and Add Issue. Add URL is enabled only for an active tab base URL that is not already in the URLs list. URLs can be deleted from the list. The Agents view manages persisted agent files, starting with `GLOBAL AGENT` from `AGENTS.md`; agents can use variables such as `{{git}}`, `{{workspace}}`, and `{{url}}` in their markdown content. Add Issue is enabled when the active tab base URL is already linked to a Gitea repo; it shows the active URL being attached, creates an issue with the selected agent and selected labels, and uploads the rendered agent file plus accumulated screenshots and files as issue assets. Captured screenshots can be expanded, removed, or cropped with a wider no-horizontal-scroll crop panel. Open Issues lists recent issues by creation time, defaults to open issues for the current repo, can expand to all loaded repos, can filter Open/Closed/Open + Closed, and lets a selected issue open directly in Gitea, edit subject/content/labels against that issue's repo with content-version readback verification, view attachments and comments, upload more screenshots/files directly to the issue, and add a comment with screenshots and file attachments. Rows can be deleted, and existing issue attachments can be deleted. +The app opens in the Chrome side panel. The top icon menu includes Add URL, URLs, Gitea repos, Workspaces, Agents, Issues, and Add Issue. Add URL is enabled only for an active tab base URL that is not already in the URLs list. URLs can be deleted from the list. The Agents view manages persisted agent files, starting with `GLOBAL AGENT` from `AGENTS.md`; agents can use variables such as `{{git}}`, `{{workspace}}`, and `{{url}}` in their markdown content. Add Issue is enabled when the active tab base URL is already linked to a Gitea repo; it shows the active URL being attached, creates an issue with the selected agent and selected labels, and uploads the rendered agent file plus accumulated screenshots and files as issue assets. Captured screenshots can be expanded, removed, or cropped with a wider no-horizontal-scroll crop panel. Issues lists recent issues by creation time, defaults to open issues for the current repo, can expand to all loaded repos, has Open/Closed status checkboxes plus an empty-by-default label filter, and lets a selected issue open directly in Gitea, log a placeholder Send to Codex payload, edit subject/content/labels against that issue's repo with content-version readback verification, view attachments and comments, upload more screenshots/files directly to the issue, and add a comment with screenshots and file attachments. Clicking the selected issue row again or the detail close control collapses the detail panel. Rows can be deleted, and existing issue attachments can be deleted. Builds generate `public/local-context.json` from `/Users/Tabitha/.openclaw/workspace_Father`. That snapshot links known workspaces to local Gitea repos and is copied into `dist/` so the built extension is self-contained. diff --git a/docs/extension-management.md b/docs/extension-management.md index a811020..b0c5e4c 100644 --- a/docs/extension-management.md +++ b/docs/extension-management.md @@ -52,20 +52,22 @@ Required variables: - Created issues must upload the selected rendered agent file as a Gitea issue asset. Supported template variables in agent content are `{{git}}`, `{{repo}}`, `{{workspace}}`, `{{url}}`, `{{baseUrl}}`, `{{profile}}`, and `{{issue}}`. - Newly created issues must use the selected labels. -## Open Issues +## Issues -- Open Issues owns existing issue browsing and follow-up comments. -- The primary Open Issues panel lists recent issues ordered by creation time. +- Issues owns existing issue browsing and follow-up comments. +- The primary Issues panel lists recent issues ordered by creation time. - The `Only current repo` checkbox defaults to checked and limits the table to the active URL's linked Gitea repo. - Turning off `Only current repo` loads recent issues from all repos already loaded in the extension. -- The status dropdown defaults to `Open` and supports `Open`, `Closed`, and `Open + Closed`. -- Selecting an issue shows a direct Open in Gitea link, editable issue subject/content/label fields, issue attachments, direct attachment upload controls, comment chain, and a comment form for text, additional screenshots, and file attachments. -- Saving selected issue edits must use the repo identity from the selected Open Issues row, include Gitea's current issue content version when available, verify the saved subject/content with a readback before reporting success, save labels through Gitea's issue labels endpoint, and must not fall back to the active tab's repo. +- Status filtering uses `Open` and `Closed` checkboxes as a multiselect. The default is `Open` checked. +- The label filter defaults to empty and filters the visible issue rows by a selected known label. +- Selecting an issue shows a direct Open in Gitea link, a Send to Codex button that currently logs the selected issue payload, editable issue subject/content/label fields, issue attachments, direct attachment upload controls, comment chain, and a comment form for text, additional screenshots, and file attachments. +- Clicking the selected issue row again or clicking the detail close control closes the selected issue detail panel without deleting or changing the issue. +- Saving selected issue edits must use the repo identity from the selected Issues row, include Gitea's current issue content version when available, verify the saved subject/content with a readback before reporting success, save labels through Gitea's issue labels endpoint, and must not fall back to the active tab's repo. - Issue comments should upload screenshots/files as issue assets first, then create a comment containing links to those attachments. - Additional selected-issue screenshots/files can also be uploaded directly as issue assets without creating a comment. - Existing issue attachments can be deleted from the selected issue. - Existing issue rows can delete the issue. Issue labels are edited in the selected issue form. Deletion must require confirmation. -- Typing in Add Issue, selected issue edit fields, Open Issues comments, or Agents editor fields must not blur the focused control. +- Typing in Add Issue, selected issue edit fields, Issues comments, or Agents editor fields must not blur the focused control. ## URL Management diff --git a/index.html b/index.html index 3f4103f..9557c03 100644 --- a/index.html +++ b/index.html @@ -30,9 +30,9 @@ Agents - + + @@ -728,16 +773,10 @@ function renderOpenIssues(): string { Only current repo - - ${ @@ -939,8 +978,18 @@ function bindViewActions(): void { appStore.getState().setOpenIssuesLimit((event.target as HTMLInputElement).checked); }); - viewContent?.querySelector("#open-issues-state-filter")?.addEventListener("change", (event) => { - appStore.getState().setOpenIssuesStateFilter((event.target as HTMLSelectElement).value as OpenIssuesStateFilter); + 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) => { @@ -1027,6 +1076,25 @@ function bindViewActions(): void { 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 @@ -1239,7 +1307,7 @@ function renderView(): void { "add-issue": "Add issue", "add-url": "Add URL", agents: "Agents", - "open-issues": "Open Issues", + "open-issues": "Issues", urls: "URLs", repos: "Gitea repos", workspaces: "Workspaces" @@ -1283,6 +1351,7 @@ function renderView(): void { } viewContent.insertAdjacentHTML("beforeend", renderScreenshotPreview()); + hydrateIcons(); bindViewActions(); } @@ -1369,8 +1438,8 @@ function render(): void { function ensureOpenIssuesLoaded(): void { const state = appStore.getState(); const scopeKey = state.openIssuesLimitToCurrentRepo - ? `current:${getActiveKnownUrl()?.giteaRepo?.fullName ?? "none"}:${state.openIssuesStateFilter}` - : `all-repos:${state.openIssuesStateFilter}`; + ? `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" && diff --git a/src/store.ts b/src/store.ts index e5720a2..b8910e6 100644 --- a/src/store.ts +++ b/src/store.ts @@ -27,7 +27,7 @@ export type StatusTone = "neutral" | "success" | "error"; export type AppView = "urls" | "repos" | "workspaces" | "agents" | "open-issues" | "add-url" | "add-issue"; export type AddUrlMode = "workspace" | "repo"; export type ScreenshotTarget = "issue" | "comment" | "issue-attachment"; -export type OpenIssuesStateFilter = GiteaIssueStateFilter; +export type OpenIssuesStatusFilter = "open" | "closed"; export const CHROME_PROFILE_NAME = "Silma"; export const ISSUE_LABEL_OPTIONS = [ @@ -174,7 +174,8 @@ interface AppState { targetIssues: GiteaIssueSummary[]; openIssues: OpenIssue[]; openIssuesLimitToCurrentRepo: boolean; - openIssuesStateFilter: OpenIssuesStateFilter; + openIssuesStatusFilters: OpenIssuesStatusFilter[]; + openIssuesLabelFilter: IssueLabelOption | ""; openIssuesScopeKey: string; issueListRepoFullName: string; selectedIssueNumber: number | null; @@ -220,9 +221,11 @@ interface AppState { applyScreenshotCrop: () => Promise; removePreviewScreenshot: () => void; setOpenIssuesLimit: (limitToCurrentRepo: boolean) => void; - setOpenIssuesStateFilter: (stateFilter: OpenIssuesStateFilter) => void; + setOpenIssuesStatusFilters: (statusFilters: OpenIssuesStatusFilter[]) => void; + setOpenIssuesLabelFilter: (labelFilter: IssueLabelOption | "") => void; setSelectedIssueEditDraft: (draft: Partial) => void; saveSelectedIssueEdits: () => Promise; + clearSelectedIssue: () => void; selectIssue: (issueNumber: number, repoFullName?: string) => void; loadOpenIssues: (force?: boolean) => Promise; loadTargetIssues: (force?: boolean) => Promise; @@ -418,7 +421,46 @@ function getIssueTarget(state: AppState, repoFullName = state.selectedIssueRepoF function getOpenIssuesScopeKey(state: AppState): string { const repoScope = state.openIssuesLimitToCurrentRepo ? `current:${getTargetRepo(state)?.fullName ?? "none"}` : "all-repos"; - return `${repoScope}:${state.openIssuesStateFilter}`; + return `${repoScope}:${state.openIssuesStatusFilters.join(",")}:${state.openIssuesLabelFilter || "all-labels"}`; +} + +function getOpenIssuesGiteaState(statusFilters: OpenIssuesStatusFilter[]): GiteaIssueStateFilter | null { + if (!statusFilters.length) { + return null; + } + + return statusFilters.length === 1 ? statusFilters[0] : "all"; +} + +function issueHasLabel(issue: GiteaIssueSummary, labelFilter: IssueLabelOption | ""): boolean { + if (!labelFilter) { + return true; + } + + return issue.labels.some((label) => label.toLowerCase() === labelFilter.toLowerCase()); +} + +function emptyIssueDrafts() { + return { + selectedIssueNumber: null, + selectedIssueRepoFullName: "", + selectedIssueDetail: null, + selectedIssueDetailNumber: null, + selectedIssueEditDraft: { + subject: "", + content: "", + labels: [] + }, + issueCommentDraft: { + content: "", + screenshots: [], + files: [] + }, + issueAttachmentDraft: { + screenshots: [], + files: [] + } + }; } function toOpenIssue(issue: GiteaIssueSummary, target: RepoTarget): OpenIssue { @@ -753,7 +795,8 @@ export const appStore = createStore()((set, get) => ({ targetIssues: [], openIssues: [], openIssuesLimitToCurrentRepo: true, - openIssuesStateFilter: "open", + openIssuesStatusFilters: ["open"], + openIssuesLabelFilter: "", openIssuesScopeKey: "", issueListRepoFullName: "", selectedIssueNumber: null, @@ -1065,92 +1108,75 @@ export const appStore = createStore()((set, get) => ({ openIssuesLimitToCurrentRepo: limitToCurrentRepo, openIssuesScopeKey: "", openIssues: [], - selectedIssueNumber: null, - selectedIssueRepoFullName: "", - selectedIssueDetail: null, - selectedIssueDetailNumber: null, - selectedIssueEditDraft: { - subject: "", - content: "", - labels: [] - }, - issueCommentDraft: { - content: "", - screenshots: [], - files: [] - }, - issueAttachmentDraft: { - screenshots: [], - files: [] - } + ...emptyIssueDrafts() }), - setOpenIssuesStateFilter: (stateFilter) => + setOpenIssuesStatusFilters: (statusFilters) => set({ - openIssuesStateFilter: stateFilter, + openIssuesStatusFilters: statusFilters, openIssuesScopeKey: "", openIssues: [], - selectedIssueNumber: null, - selectedIssueRepoFullName: "", - selectedIssueDetail: null, - selectedIssueDetailNumber: null, - selectedIssueEditDraft: { - subject: "", - content: "", - labels: [] - }, - issueCommentDraft: { - content: "", - screenshots: [], - files: [] - }, - issueAttachmentDraft: { - screenshots: [], - files: [] - } + ...emptyIssueDrafts() }), + setOpenIssuesLabelFilter: (labelFilter) => + set({ + openIssuesLabelFilter: labelFilter, + openIssuesScopeKey: "", + openIssues: [], + ...emptyIssueDrafts() + }), + clearSelectedIssue: () => set(emptyIssueDrafts()), selectIssue: (issueNumber, repoFullName) => - set((state) => ({ - selectedIssueNumber: issueNumber, - selectedIssueRepoFullName: repoFullName ?? state.selectedIssueRepoFullName ?? getTargetRepo(state)?.fullName ?? "", - selectedIssueDetail: - state.selectedIssueDetailNumber === issueNumber && - (!repoFullName || state.selectedIssueRepoFullName === repoFullName) - ? state.selectedIssueDetail - : null, - selectedIssueDetailNumber: - state.selectedIssueDetailNumber === issueNumber && - (!repoFullName || state.selectedIssueRepoFullName === repoFullName) - ? state.selectedIssueDetailNumber - : null, - selectedIssueEditDraft: - state.selectedIssueDetailNumber === issueNumber && - (!repoFullName || state.selectedIssueRepoFullName === repoFullName) && - state.selectedIssueDetail - ? { - subject: state.selectedIssueDetail.issue.title, - content: state.selectedIssueDetail.issue.body, - labels: normalizeIssueLabels(state.selectedIssueDetail.issue.labels) - } - : { - subject: "", - content: "", - labels: [] - }, - issueCommentDraft: { - content: "", - screenshots: [], - files: [] - }, - issueAttachmentDraft: { - screenshots: [], - files: [] + set((state) => { + const selectedRepoFullName = repoFullName ?? state.selectedIssueRepoFullName ?? getTargetRepo(state)?.fullName ?? ""; + + if (state.selectedIssueNumber === issueNumber && state.selectedIssueRepoFullName === selectedRepoFullName) { + return emptyIssueDrafts(); } - })), + + return { + selectedIssueNumber: issueNumber, + selectedIssueRepoFullName: selectedRepoFullName, + selectedIssueDetail: + state.selectedIssueDetailNumber === issueNumber && + (!repoFullName || state.selectedIssueRepoFullName === repoFullName) + ? state.selectedIssueDetail + : null, + selectedIssueDetailNumber: + state.selectedIssueDetailNumber === issueNumber && + (!repoFullName || state.selectedIssueRepoFullName === repoFullName) + ? state.selectedIssueDetailNumber + : null, + selectedIssueEditDraft: + state.selectedIssueDetailNumber === issueNumber && + (!repoFullName || state.selectedIssueRepoFullName === repoFullName) && + state.selectedIssueDetail + ? { + subject: state.selectedIssueDetail.issue.title, + content: state.selectedIssueDetail.issue.body, + labels: normalizeIssueLabels(state.selectedIssueDetail.issue.labels) + } + : { + subject: "", + content: "", + labels: [] + }, + issueCommentDraft: { + content: "", + screenshots: [], + files: [] + }, + issueAttachmentDraft: { + screenshots: [], + files: [] + } + }; + }), loadOpenIssues: async (force = false) => { const state = appStore.getState(); const scopeKey = getOpenIssuesScopeKey(state); + const giteaState = getOpenIssuesGiteaState(state.openIssuesStatusFilters); - if (!force && state.openIssuesScopeKey === scopeKey && state.openIssues.length > 0) { + if (!force && state.openIssuesScopeKey === scopeKey) { return; } @@ -1160,6 +1186,15 @@ export const appStore = createStore()((set, get) => ({ }); try { + if (!giteaState) { + set({ + openIssues: [], + openIssuesScopeKey: scopeKey, + ...emptyIssueDrafts() + }); + return; + } + const targets = state.openIssuesLimitToCurrentRepo ? [getTargetRepo(state)].filter((target): target is RepoTarget => Boolean(target)) : state.repos @@ -1169,15 +1204,7 @@ export const appStore = createStore()((set, get) => ({ if (!targets.length) { set({ openIssues: [], - selectedIssueNumber: null, - selectedIssueRepoFullName: "", - selectedIssueDetail: null, - selectedIssueDetailNumber: null, - selectedIssueEditDraft: { - subject: "", - content: "", - labels: [] - } + ...emptyIssueDrafts() }); return; } @@ -1189,13 +1216,14 @@ export const appStore = createStore()((set, get) => ({ limit: 25, order: "desc", sort: "created", - state: state.openIssuesStateFilter + state: giteaState }) ).map((issue) => toOpenIssue(issue, target)) ) ); const openIssues = issueGroups .flat() + .filter((issue) => issueHasLabel(issue, state.openIssuesLabelFilter)) .sort((a, b) => { const aTime = a.createdAt ? Date.parse(a.createdAt) : 0; const bTime = b.createdAt ? Date.parse(b.createdAt) : 0; @@ -1367,7 +1395,7 @@ export const appStore = createStore()((set, get) => ({ const subject = state.selectedIssueEditDraft.subject.trim(); if (!target || !issueNumber) { - setStatus(set, "Select an issue from Open Issues before editing it.", "error"); + setStatus(set, "Select an issue from Issues before editing it.", "error"); return; } diff --git a/src/styles.css b/src/styles.css index 5a1412a..f97ed43 100644 --- a/src/styles.css +++ b/src/styles.css @@ -370,6 +370,26 @@ a.item-title:hover, font-size: 10px; } +.icon-only-button { + display: inline-grid; + place-items: center; + width: 26px; + min-width: 26px; + min-height: 24px; + padding: 3px; +} + +.icon-only-button svg, +.secondary-button svg { + width: 14px; + height: 14px; + stroke-width: 2.4; +} + +.secondary-button span { + display: inline-block; +} + .secondary-button { border: 1px solid #9fb3c8; background: #eef4f9; @@ -605,6 +625,12 @@ input[readonly] { gap: 5px; } +.issue-detail-actions .secondary-button:not(.icon-only-button) { + display: inline-flex; + align-items: center; + gap: 4px; +} + .inline-heading { padding: 0; } @@ -688,10 +714,30 @@ input[readonly] { .issue-filter-row { display: flex; align-items: center; - justify-content: space-between; + flex-wrap: wrap; + justify-content: flex-start; gap: 8px; } +.inline-fieldset { + display: flex; + align-items: center; + gap: 6px; + border: 0; + padding: 0; +} + +.inline-fieldset legend { + color: #52616f; + font-size: 10px; + font-weight: 850; + text-transform: uppercase; +} + +.status-filter-fieldset .checkbox-row { + gap: 3px; +} + .compact-select-label { display: flex; align-items: center;