@@ -49,17 +49,18 @@ Use `.env.local` for local secrets. Keep `.env.local` and any real token-bearing
|
||||
- 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.
|
||||
- The default `GLOBAL AGENT` is backed by `AGENTS.md` and contains agnostic handoff rules with template variables such as `{{git}}`, `{{workspace}}`, `{{url}}`, `{{baseUrl}}`, `{{profile}}`, and `{{issue}}`.
|
||||
- Add Issue is enabled only when the active tab's base URL is already in the URLs list and has a linked Gitea repo. The issue form must show the target repo, the active URL the extension believes it is attached to, let the user select an agent file, capture visible-tab screenshots, accept multiple accumulated Gitea-friendly file attachments, and include `Chrome profile: Silma` plus the selected agent in created issue bodies.
|
||||
- Add Issue is enabled only when the active tab's base URL is already in the URLs list and has a linked Gitea repo. The issue form must show the target repo, the active URL the extension believes it is attached to, let the user select an agent file and issue labels, capture visible-tab screenshots, accept multiple accumulated Gitea-friendly file attachments, and include `Chrome profile: Silma` plus the selected agent in created issue bodies.
|
||||
- Clicking a captured screenshot must open an expanded preview with Close, Remove, and Crop controls. The crop area should be a simple draggable/resizable box without a shaded crop fill, and the preview panel should use the available viewport without horizontal scrolling.
|
||||
- Newly created issues must be created with the `Change Request` label.
|
||||
- Label options for issue creation and editing are `Change Request`, `Idea`, `Tabitha`, `Orson`, `Belisarius`, `Ozymandias`, and `Ready`.
|
||||
- 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.
|
||||
- If the Ready checkbox is checked, create the issue first, upload the rendered agent file plus screenshots/files as issue assets second, and apply the `ready` label last.
|
||||
- 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 fields, issue attachments, and comment chain, followed by 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 and must never fall back to the active tab's repo.
|
||||
- Existing issue rows must allow applying the `ready` label and deleting the issue. Existing issue attachments must be removable. Deletion must require user confirmation.
|
||||
- Existing issue rows must allow deleting the issue. Existing issue labels must be editable from the selected issue form. Existing issue attachments must be removable. 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.
|
||||
- Keep the Add Issue view dense; do not show the redundant view header title/description inside this view, and keep the Ready checkbox label short.
|
||||
- Keep the Add Issue view dense; do not show the redundant view header title/description inside this view.
|
||||
- The extension requests `activeTab` and `<all_urls>` host permission so screenshots can be captured from the current Chrome tab even after the side panel has stayed open across tab changes.
|
||||
|
||||
## Chrome Extension Management
|
||||
|
||||
@@ -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 `Change Request` label, uploads the rendered agent file plus accumulated screenshots and files as issue assets, and can apply the `ready` label after uploads. 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 against that issue's repo, view attachments and comments, and add a comment with screenshots and file attachments. Rows can be marked Ready or 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, 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, view attachments and comments, and add a comment with screenshots and file attachments. 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.
|
||||
|
||||
|
||||
@@ -40,15 +40,17 @@ Required variables:
|
||||
- Add Issue is enabled only when the active tab's base URL is already in the URLs list and that URL is linked to a Gitea repo.
|
||||
- The Add Issue view must show the Gitea repo that will receive the issue and the active URL the extension believes it is attached to.
|
||||
- The Add Issue form must let the user select the agent file for handoff. Agents are managed in the Agents view, persisted in `chrome.storage.local`, and the initial agent is `GLOBAL AGENT` backed by `AGENTS.md`.
|
||||
- The Add Issue view intentionally omits the redundant view header title/description and keeps the Ready checkbox label short.
|
||||
- The Add Issue view intentionally omits the redundant view header title/description.
|
||||
- Add Issue and selected issue editing expose these label options: `Change Request`, `Idea`, `Tabitha`, `Orson`, `Belisarius`, `Ozymandias`, and `Ready`.
|
||||
- New issues default to `Change Request` selected unless the user changes the label selection.
|
||||
- Do not show the old Ready checkbox; `Ready` is now a selectable label.
|
||||
- Screenshot capture uses Chrome's visible-tab capture API and requires the extension's `activeTab` permission plus `<all_urls>` host permission so capture still works after the side panel remains open across tab changes.
|
||||
- Captured screenshots should appear immediately in Add Issue as thumbnail rows.
|
||||
- Captured screenshots can be expanded, closed, removed, or cropped before upload. The crop UI should use a plain draggable/resizable crop box instead of shaded slider-based controls, and the preview panel should fit the available viewport without horizontal scrolling.
|
||||
- File attachments are accumulated across picker selections and all selected files must be uploaded to Gitea.
|
||||
- Created issue bodies must include `Chrome profile: Silma` and the selected agent.
|
||||
- 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 include the `Change Request` label.
|
||||
- When Ready is checked, create the issue, upload the rendered agent file plus screenshots/files as Gitea issue assets, then apply the `ready` label last.
|
||||
- Newly created issues must use the selected labels.
|
||||
|
||||
## Open Issues
|
||||
|
||||
@@ -57,11 +59,11 @@ Required variables:
|
||||
- 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 fields, issue attachments, comment chain, and a comment form for text, additional screenshots, and file attachments.
|
||||
- Selecting an issue shows a direct Open in Gitea link, editable issue subject/content/label fields, issue attachments, 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 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.
|
||||
- Existing issue attachments can be deleted from the selected issue.
|
||||
- Existing issue rows can apply the `ready` label or delete the issue. Deletion must require confirmation.
|
||||
- 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.
|
||||
|
||||
## URL Management
|
||||
|
||||
@@ -19,7 +19,7 @@ Important files:
|
||||
|
||||
Build with `npm run build`. The deployable output directory is `dist/`; load that folder in Chrome as an unpacked extension or point deployment tooling at it. Do not edit `dist/` directly.
|
||||
|
||||
UI behavior: the app renders as a Chrome side panel, not a draggable window. The menu items are Add URL, URLs, Gitea Repos, Workspaces, Agents, Open Issues, and Add Issue. Add URL is enabled only when the active tab base URL is not already known; the user must choose either a workspace or a Gitea repo, and the app infers the linked counterpart. URLs can be deleted from the list; user-added URLs are removed from storage, while generated known URLs are hidden through stored deleted bases. The Agents view manages persisted agent files and starts with `GLOBAL AGENT` backed by `AGENTS.md`; agent markdown can use `{{git}}`, `{{repo}}`, `{{workspace}}`, `{{url}}`, `{{baseUrl}}`, `{{profile}}`, and `{{issue}}` template variables. Add Issue is enabled only when the active tab base URL is known and linked to a Gitea repo. It shows the target repo and active URL, captures visible-tab screenshots with thumbnails, accepts accumulated file attachments, lets the user select an agent file, creates the Gitea issue with the `Change Request` label, uploads the rendered agent file plus all screenshots/files, then applies the `ready` label if requested. Captured screenshots can be expanded, removed, or cropped with a wider no-horizontal-scroll draggable/resizable crop panel. Created issue bodies include `Chrome profile: Silma` and the selected agent. Open Issues lists recent issues by creation time, defaults to open issues for the active URL's linked repo through an `Only current repo` checkbox, can load recent issues across all loaded repos, supports Open/Closed/Open + Closed status filtering, and shows selected issue subject/content edit fields that save against the selected issue repo, a direct Open in Gitea link, attachments, comment chains, and a comment form with text, screenshots, and file attachments. Existing issue rows can be marked Ready or deleted after confirmation, and existing issue attachments can be deleted. Text-entry state updates must not blur focused Add Issue, selected issue edit, Open Issues comment, or Agents editor controls. Gitea repos are ordered by latest default-branch commit. Workspace and URL views show established links between local workspaces, known URLs, and Gitea repos.
|
||||
UI behavior: the app renders as a Chrome side panel, not a draggable window. The menu items are Add URL, URLs, Gitea Repos, Workspaces, Agents, Open Issues, and Add Issue. Add URL is enabled only when the active tab base URL is not already known; the user must choose either a workspace or a Gitea repo, and the app infers the linked counterpart. URLs can be deleted from the list; user-added URLs are removed from storage, while generated known URLs are hidden through stored deleted bases. The Agents view manages persisted agent files and starts with `GLOBAL AGENT` backed by `AGENTS.md`; agent markdown can use `{{git}}`, `{{repo}}`, `{{workspace}}`, `{{url}}`, `{{baseUrl}}`, `{{profile}}`, and `{{issue}}` template variables. Add Issue is enabled only when the active tab base URL is known and linked to a Gitea repo. It shows the target repo and active URL, captures visible-tab screenshots with thumbnails, accepts accumulated file attachments, lets the user select an agent file and issue labels, creates the Gitea issue with the selected labels, and uploads the rendered agent file plus all screenshots/files. The label options are Change Request, Idea, Tabitha, Orson, Belisarius, Ozymandias, and Ready; new issues default to Change Request. Captured screenshots can be expanded, removed, or cropped with a wider no-horizontal-scroll draggable/resizable crop panel. Created issue bodies include `Chrome profile: Silma` and the selected agent. Open Issues lists recent issues by creation time, defaults to open issues for the active URL's linked repo through an `Only current repo` checkbox, can load recent issues across all loaded repos, supports Open/Closed/Open + Closed status filtering, and shows selected issue subject/content/label edit fields that save against the selected issue repo, a direct Open in Gitea link, attachments, comment chains, and a comment form with text, screenshots, and file attachments. Existing issue rows can be deleted after confirmation, and existing issue attachments can be deleted. Text-entry state updates must not blur focused Add Issue, selected issue edit, Open Issues comment, or Agents editor controls. Gitea repos are ordered by latest default-branch commit. Workspace and URL views show established links between local workspaces, known URLs, and Gitea repos.
|
||||
|
||||
Extension management rule: use the Codex Chrome extension with the Chrome profile named `Silma`. Do not use `Profile 1`.
|
||||
|
||||
|
||||
+2
-2
@@ -59,9 +59,9 @@
|
||||
"agents_behavior": "The Agents view manages persisted agent files. The initial agent is GLOBAL AGENT backed by AGENTS.md, and agent markdown supports {{git}}, {{repo}}, {{workspace}}, {{url}}, {{baseUrl}}, {{profile}}, and {{issue}} template variables.",
|
||||
"add_url_behavior": "The user must pick either a workspace or a Gitea repo. The app infers the linked counterpart from local context and stores the URL link in chrome.storage.local.",
|
||||
"delete_url_behavior": "URLs can be deleted from the list. User-added URLs are removed from chrome.storage.local; generated known URLs are hidden by storing their base URL in silmaAide.deletedUrlBases.",
|
||||
"add_issue_behavior": "The form shows the target repo and active URL for the active known URL, lets the user select an agent file, captures visible-tab screenshots with thumbnails, accepts accumulated file attachments, creates a Gitea issue with the Change Request label, uploads the rendered agent file plus all screenshots/files as issue assets, then applies the ready label after uploads when requested. Captured screenshots can be expanded, removed, or cropped with a wider no-horizontal-scroll draggable/resizable crop panel. Issue bodies include Chrome profile: Silma and the selected agent. The view intentionally omits the redundant Add Issue header copy and keeps the Ready checkbox label short.",
|
||||
"add_issue_behavior": "The form shows the target repo and active URL for the active known URL, lets the user select an agent file and issue labels, captures visible-tab screenshots with thumbnails, accepts accumulated file attachments, creates a Gitea issue with the selected labels, and uploads the rendered agent file plus all screenshots/files as issue assets. Label options are Change Request, Idea, Tabitha, Orson, Belisarius, Ozymandias, and Ready; new issues default to Change Request. Captured screenshots can be expanded, removed, or cropped with a wider no-horizontal-scroll draggable/resizable crop panel. Issue bodies include Chrome profile: Silma and the selected agent. The view intentionally omits the redundant Add Issue header copy.",
|
||||
"open_issues_behavior": "Open Issues lists recent issues ordered by creation time. The Only current repo checkbox defaults to true and limits rows to the active URL's linked Gitea repo; disabling it loads recent issues from all repos already loaded by the extension. The status dropdown defaults to Open and supports Open, Closed, and Open + Closed.",
|
||||
"issue_comment_behavior": "Open Issues owns existing issue follow-up. Selecting an issue shows a direct Open in Gitea link, editable issue subject/content fields that save against the selected issue repo, issue attachments, comment chain, and a comment form that accepts text, screenshots, and file attachments, uploads attachments as issue assets, and submits a Gitea issue comment with attachment links. Existing issue attachments can be deleted. Issue rows can apply the ready label or delete the issue after confirmation. Draft text-entry updates must not blur focused Add Issue, selected issue edit, Open Issues comment, or Agents editor controls.",
|
||||
"issue_comment_behavior": "Open Issues owns existing issue follow-up. Selecting an issue shows a direct Open in Gitea link, editable issue subject/content/label fields that save against the selected issue repo, issue attachments, comment chain, and a comment form that accepts text, screenshots, and file attachments, uploads attachments as issue assets, and submits a Gitea issue comment with attachment links. Existing issue attachments can be deleted. Issue rows can delete the issue after confirmation. Draft text-entry updates must not blur focused Add Issue, selected issue edit, Open Issues comment, or Agents editor controls.",
|
||||
"known_urls": [
|
||||
{
|
||||
"url": "http://100.66.226.22:23030/",
|
||||
|
||||
+21
-13
@@ -51,6 +51,7 @@ interface GiteaIssueResponse {
|
||||
login?: string;
|
||||
};
|
||||
comments?: number;
|
||||
labels?: GiteaLabelResponse[];
|
||||
}
|
||||
|
||||
interface GiteaIssueCommentResponse {
|
||||
@@ -67,6 +68,7 @@ interface GiteaIssueCommentResponse {
|
||||
interface GiteaLabelResponse {
|
||||
id: number;
|
||||
name: string;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
interface GiteaAttachmentResponse {
|
||||
@@ -93,6 +95,7 @@ export interface GiteaIssueSummary {
|
||||
updatedAt: string;
|
||||
author: string;
|
||||
comments: number;
|
||||
labels: string[];
|
||||
}
|
||||
|
||||
export interface GiteaIssueAttachment {
|
||||
@@ -143,7 +146,8 @@ function toIssueSummary(issue: GiteaIssueResponse, owner: string, repo: string,
|
||||
createdAt: issue.created_at || "",
|
||||
updatedAt: issue.updated_at || "",
|
||||
author: issue.user?.login || "unknown",
|
||||
comments: issue.comments ?? 0
|
||||
comments: issue.comments ?? 0,
|
||||
labels: issue.labels?.map((label) => label.name).filter(Boolean) ?? []
|
||||
};
|
||||
}
|
||||
|
||||
@@ -245,6 +249,22 @@ export async function updateGiteaIssue(
|
||||
}
|
||||
}
|
||||
|
||||
export async function setGiteaIssueLabels(
|
||||
owner: string,
|
||||
repo: string,
|
||||
issueNumber: number,
|
||||
labelIds: number[]
|
||||
): Promise<void> {
|
||||
const client = createGiteaClient();
|
||||
try {
|
||||
await client.put(`/repos/${owner}/${repo}/issues/${issueNumber}/labels`, {
|
||||
labels: labelIds
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(getGiteaErrorMessage(error, "Could not update the Gitea issue labels."));
|
||||
}
|
||||
}
|
||||
|
||||
export async function getGiteaIssues(
|
||||
owner: string,
|
||||
repo: string,
|
||||
@@ -396,18 +416,6 @@ export async function ensureGiteaLabel(owner: string, repo: string, name: string
|
||||
}
|
||||
}
|
||||
|
||||
export async function addGiteaIssueLabels(
|
||||
owner: string,
|
||||
repo: string,
|
||||
issueNumber: number,
|
||||
labelIds: number[]
|
||||
): Promise<void> {
|
||||
const client = createGiteaClient();
|
||||
await client.post(`/repos/${owner}/${repo}/issues/${issueNumber}/labels`, {
|
||||
labels: labelIds
|
||||
});
|
||||
}
|
||||
|
||||
export async function getReposOrderedByLastCommit(): Promise<GiteaRepoSummary[]> {
|
||||
const client = axios.create({
|
||||
baseURL: getApiBaseUrl(giteaConfig.baseUrl),
|
||||
|
||||
+47
-25
@@ -4,9 +4,11 @@ import {
|
||||
appStore,
|
||||
CHROME_PROFILE_NAME,
|
||||
findKnownUrlForBase,
|
||||
ISSUE_LABEL_OPTIONS,
|
||||
type AgentProfile,
|
||||
type AddUrlMode,
|
||||
type AppView,
|
||||
type IssueLabelOption,
|
||||
type OpenIssue,
|
||||
type OpenIssuesStateFilter,
|
||||
type ScreenshotAttachment
|
||||
@@ -389,6 +391,24 @@ function renderAgentOptions(agents: AgentProfile[], selectedAgentId: string): st
|
||||
.join("");
|
||||
}
|
||||
|
||||
function renderLabelOptions(selectedLabels: IssueLabelOption[], target: "draft" | "edit"): string {
|
||||
return `
|
||||
<fieldset class="label-fieldset">
|
||||
<legend>Labels</legend>
|
||||
<div class="label-option-grid">
|
||||
${ISSUE_LABEL_OPTIONS.map(
|
||||
(label) => `
|
||||
<label class="checkbox-row label-option">
|
||||
<input type="checkbox" value="${escapeHtml(label)}" data-label-target="${target}" ${selectedLabels.includes(label) ? "checked" : ""} />
|
||||
<span>${escapeHtml(label)}</span>
|
||||
</label>
|
||||
`
|
||||
).join("")}
|
||||
</div>
|
||||
</fieldset>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderBodyText(body: string, emptyText: string): string {
|
||||
const trimmed = body.trim();
|
||||
if (!trimmed) {
|
||||
@@ -540,7 +560,6 @@ function renderIssueRows(issues: OpenIssue[], selectedIssueNumber: number | null
|
||||
<span class="issue-state">${escapeHtml(issue.state)}</span>
|
||||
<span class="issue-meta">${formatDate(issue.createdAt)}</span>
|
||||
</button>
|
||||
<button class="secondary-button compact-button" type="button" data-ready-issue="${issue.number}" data-issue-repo="${escapeHtml(issue.repoFullName)}" ${state.isUpdatingIssue ? "disabled" : ""}>Ready</button>
|
||||
<button class="danger-button compact-button" type="button" data-delete-issue="${issue.number}" data-issue-repo="${escapeHtml(issue.repoFullName)}" ${state.isUpdatingIssue ? "disabled" : ""}>Delete</button>
|
||||
</div>
|
||||
`
|
||||
@@ -606,7 +625,11 @@ function renderSelectedIssueDetail(): string {
|
||||
}
|
||||
|
||||
const draft = state.selectedIssueEditDraft;
|
||||
const hasChanges = draft.subject !== detail.issue.title || draft.content !== detail.issue.body;
|
||||
const detailLabels = ISSUE_LABEL_OPTIONS.filter((label) => detail.issue.labels.includes(label));
|
||||
const hasChanges =
|
||||
draft.subject !== detail.issue.title ||
|
||||
draft.content !== detail.issue.body ||
|
||||
draft.labels.join("|") !== detailLabels.join("|");
|
||||
|
||||
return `
|
||||
<article class="item-card issue-detail-card">
|
||||
@@ -629,6 +652,7 @@ function renderSelectedIssueDetail(): string {
|
||||
<span>Content</span>
|
||||
<textarea id="selected-issue-content" rows="8">${escapeHtml(draft.content)}</textarea>
|
||||
</label>
|
||||
${renderLabelOptions(draft.labels, "edit")}
|
||||
<button id="save-selected-issue" class="primary-button" type="button" ${hasChanges && draft.subject.trim() && !state.isUpdatingIssue ? "" : "disabled"}>
|
||||
${state.isUpdatingIssue ? "Saving issue..." : "Save issue"}
|
||||
</button>
|
||||
@@ -741,6 +765,8 @@ function renderAddIssue(): string {
|
||||
</select>
|
||||
</label>
|
||||
|
||||
${renderLabelOptions(draft.labels, "draft")}
|
||||
|
||||
<section class="attachment-section">
|
||||
<div class="attachment-heading">
|
||||
<span>Screenshots</span>
|
||||
@@ -757,11 +783,6 @@ function renderAddIssue(): string {
|
||||
</label>
|
||||
${renderFileList(draft.files, "issue")}
|
||||
|
||||
<label class="checkbox-row">
|
||||
<input id="issue-ready" type="checkbox" ${draft.ready ? "checked" : ""} />
|
||||
<span>Ready</span>
|
||||
</label>
|
||||
|
||||
<button id="create-issue" class="primary-button" type="button" ${draft.subject.trim() && !state.isCreatingIssue ? "" : "disabled"}>
|
||||
${state.isCreatingIssue ? "Creating issue..." : "Create issue"}
|
||||
</button>
|
||||
@@ -893,12 +914,6 @@ function bindViewActions(): void {
|
||||
});
|
||||
});
|
||||
|
||||
viewContent?.querySelector<HTMLInputElement>("#issue-ready")?.addEventListener("change", (event) => {
|
||||
appStore.getState().setIssueDraft({
|
||||
ready: (event.target as HTMLInputElement).checked
|
||||
});
|
||||
});
|
||||
|
||||
viewContent?.querySelector<HTMLInputElement>("#issue-files")?.addEventListener("change", (event) => {
|
||||
const input = event.target as HTMLInputElement;
|
||||
appStore.getState().setIssueFiles(Array.from(input.files ?? []));
|
||||
@@ -947,16 +962,6 @@ function bindViewActions(): void {
|
||||
});
|
||||
});
|
||||
|
||||
viewContent?.querySelectorAll<HTMLButtonElement>("[data-ready-issue]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const issueNumber = Number(button.dataset.readyIssue);
|
||||
const repoFullName = button.dataset.issueRepo;
|
||||
if (Number.isFinite(issueNumber)) {
|
||||
void appStore.getState().markIssueReady(issueNumber, repoFullName);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
viewContent?.querySelectorAll<HTMLButtonElement>("[data-delete-issue]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const issueNumber = Number(button.dataset.deleteIssue);
|
||||
@@ -991,6 +996,21 @@ function bindViewActions(): void {
|
||||
void appStore.getState().saveSelectedIssueEdits();
|
||||
});
|
||||
|
||||
viewContent?.querySelectorAll<HTMLInputElement>("[data-label-target]").forEach((input) => {
|
||||
input.addEventListener("change", () => {
|
||||
const target = input.dataset.labelTarget;
|
||||
const labels = Array.from(
|
||||
viewContent.querySelectorAll<HTMLInputElement>(`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<HTMLButtonElement>("[data-delete-issue-attachment]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const attachmentId = Number(button.dataset.deleteIssueAttachment);
|
||||
@@ -1221,7 +1241,7 @@ function shouldRenderView(): boolean {
|
||||
}
|
||||
|
||||
return !activeElement.matches(
|
||||
"#issue-subject, #issue-content, #issue-agent, #issue-ready, #selected-issue-subject, #selected-issue-content, #issue-comment-content, #agent-name, #agent-file, #agent-description, #agent-content"
|
||||
"#issue-subject, #issue-content, #issue-agent, #selected-issue-subject, #selected-issue-content, #issue-comment-content, #agent-name, #agent-file, #agent-description, #agent-content, [data-label-target]"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1244,11 +1264,13 @@ function syncFocusedFormControls(): void {
|
||||
}
|
||||
|
||||
if (saveSelectedIssue && state.selectedIssueDetail) {
|
||||
const detailLabels = ISSUE_LABEL_OPTIONS.filter((label) => state.selectedIssueDetail?.issue.labels.includes(label));
|
||||
saveSelectedIssue.disabled =
|
||||
!state.selectedIssueEditDraft.subject.trim() ||
|
||||
state.isUpdatingIssue ||
|
||||
(state.selectedIssueEditDraft.subject === state.selectedIssueDetail.issue.title &&
|
||||
state.selectedIssueEditDraft.content === state.selectedIssueDetail.issue.body);
|
||||
state.selectedIssueEditDraft.content === state.selectedIssueDetail.issue.body &&
|
||||
state.selectedIssueEditDraft.labels.join("|") === detailLabels.join("|"));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+58
-51
@@ -1,6 +1,5 @@
|
||||
import { createStore } from "zustand/vanilla";
|
||||
import {
|
||||
addGiteaIssueLabels,
|
||||
createGiteaIssueComment,
|
||||
createGiteaIssue,
|
||||
deleteGiteaIssue,
|
||||
@@ -11,6 +10,7 @@ import {
|
||||
getGiteaIssues,
|
||||
getReposOrderedByLastCommit,
|
||||
giteaConfig,
|
||||
setGiteaIssueLabels,
|
||||
type GiteaIssueAttachment,
|
||||
type GiteaIssueDetail,
|
||||
type GiteaIssueStateFilter,
|
||||
@@ -30,6 +30,17 @@ export type ScreenshotTarget = "issue" | "comment";
|
||||
export type OpenIssuesStateFilter = GiteaIssueStateFilter;
|
||||
|
||||
export const CHROME_PROFILE_NAME = "Silma";
|
||||
export const ISSUE_LABEL_OPTIONS = [
|
||||
"Change Request",
|
||||
"Idea",
|
||||
"Tabitha",
|
||||
"Orson",
|
||||
"Belisarius",
|
||||
"Ozymandias",
|
||||
"Ready"
|
||||
] as const;
|
||||
|
||||
export type IssueLabelOption = (typeof ISSUE_LABEL_OPTIONS)[number];
|
||||
|
||||
export interface AgentProfile {
|
||||
id: string;
|
||||
@@ -107,7 +118,7 @@ interface IssueDraft {
|
||||
subject: string;
|
||||
content: string;
|
||||
agentId: string;
|
||||
ready: boolean;
|
||||
labels: IssueLabelOption[];
|
||||
screenshots: ScreenshotAttachment[];
|
||||
files: File[];
|
||||
}
|
||||
@@ -121,6 +132,7 @@ interface IssueCommentDraft {
|
||||
interface IssueEditDraft {
|
||||
subject: string;
|
||||
content: string;
|
||||
labels: IssueLabelOption[];
|
||||
}
|
||||
|
||||
interface CropArea {
|
||||
@@ -204,7 +216,6 @@ interface AppState {
|
||||
loadOpenIssues: (force?: boolean) => Promise<void>;
|
||||
loadTargetIssues: (force?: boolean) => Promise<void>;
|
||||
loadSelectedIssueDetail: (force?: boolean) => Promise<void>;
|
||||
markIssueReady: (issueNumber: number, repoFullName?: string) => Promise<void>;
|
||||
deleteIssue: (issueNumber: number, repoFullName?: string) => Promise<void>;
|
||||
deleteSelectedIssueAttachment: (attachmentId: number) => Promise<void>;
|
||||
captureIssueCommentScreenshot: () => Promise<void>;
|
||||
@@ -544,6 +555,15 @@ function normalizeAgents(agents: AgentProfile[]): AgentProfile[] {
|
||||
return normalized.length ? normalized : AVAILABLE_AGENTS;
|
||||
}
|
||||
|
||||
function normalizeIssueLabels(labels: string[]): IssueLabelOption[] {
|
||||
return ISSUE_LABEL_OPTIONS.filter((option) => labels.some((label) => label.toLowerCase() === option.toLowerCase()));
|
||||
}
|
||||
|
||||
async function ensureIssueLabels(owner: string, repo: string, labels: IssueLabelOption[]): Promise<number[]> {
|
||||
const ensuredLabels = await Promise.all(labels.map((label) => ensureGiteaLabel(owner, repo, label)));
|
||||
return ensuredLabels.map((label) => label.id);
|
||||
}
|
||||
|
||||
function getAgentById(id: string, agents: AgentProfile[]): AgentProfile {
|
||||
return agents.find((agent) => agent.id === id) ?? agents[0] ?? AVAILABLE_AGENTS[0];
|
||||
}
|
||||
@@ -693,7 +713,7 @@ export const appStore = createStore<AppState>()((set, get) => ({
|
||||
subject: "",
|
||||
content: "",
|
||||
agentId: AVAILABLE_AGENTS[0].id,
|
||||
ready: false,
|
||||
labels: ["Change Request"],
|
||||
screenshots: [],
|
||||
files: []
|
||||
},
|
||||
@@ -718,7 +738,8 @@ export const appStore = createStore<AppState>()((set, get) => ({
|
||||
selectedIssueDetailNumber: null,
|
||||
selectedIssueEditDraft: {
|
||||
subject: "",
|
||||
content: ""
|
||||
content: "",
|
||||
labels: []
|
||||
},
|
||||
isLoadingData: false,
|
||||
isLoadingTargetIssues: false,
|
||||
@@ -984,7 +1005,8 @@ export const appStore = createStore<AppState>()((set, get) => ({
|
||||
selectedIssueDetailNumber: null,
|
||||
selectedIssueEditDraft: {
|
||||
subject: "",
|
||||
content: ""
|
||||
content: "",
|
||||
labels: []
|
||||
},
|
||||
issueCommentDraft: {
|
||||
content: "",
|
||||
@@ -1003,7 +1025,8 @@ export const appStore = createStore<AppState>()((set, get) => ({
|
||||
selectedIssueDetailNumber: null,
|
||||
selectedIssueEditDraft: {
|
||||
subject: "",
|
||||
content: ""
|
||||
content: "",
|
||||
labels: []
|
||||
},
|
||||
issueCommentDraft: {
|
||||
content: "",
|
||||
@@ -1031,11 +1054,13 @@ export const appStore = createStore<AppState>()((set, get) => ({
|
||||
state.selectedIssueDetail
|
||||
? {
|
||||
subject: state.selectedIssueDetail.issue.title,
|
||||
content: state.selectedIssueDetail.issue.body
|
||||
content: state.selectedIssueDetail.issue.body,
|
||||
labels: normalizeIssueLabels(state.selectedIssueDetail.issue.labels)
|
||||
}
|
||||
: {
|
||||
subject: "",
|
||||
content: ""
|
||||
content: "",
|
||||
labels: []
|
||||
},
|
||||
issueCommentDraft: {
|
||||
content: "",
|
||||
@@ -1072,7 +1097,8 @@ export const appStore = createStore<AppState>()((set, get) => ({
|
||||
selectedIssueDetailNumber: null,
|
||||
selectedIssueEditDraft: {
|
||||
subject: "",
|
||||
content: ""
|
||||
content: "",
|
||||
labels: []
|
||||
}
|
||||
});
|
||||
return;
|
||||
@@ -1148,7 +1174,8 @@ export const appStore = createStore<AppState>()((set, get) => ({
|
||||
? current.selectedIssueEditDraft
|
||||
: {
|
||||
subject: "",
|
||||
content: ""
|
||||
content: "",
|
||||
labels: []
|
||||
}
|
||||
}));
|
||||
} catch (error) {
|
||||
@@ -1236,7 +1263,8 @@ export const appStore = createStore<AppState>()((set, get) => ({
|
||||
selectedIssueDetailNumber: issueNumber,
|
||||
selectedIssueEditDraft: {
|
||||
subject: selectedIssueDetail.issue.title,
|
||||
content: selectedIssueDetail.issue.body
|
||||
content: selectedIssueDetail.issue.body,
|
||||
labels: normalizeIssueLabels(selectedIssueDetail.issue.labels)
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -1274,11 +1302,17 @@ export const appStore = createStore<AppState>()((set, get) => ({
|
||||
setStatus(set, `Saving issue #${issueNumber}...`, "neutral", { autoClear: false });
|
||||
|
||||
try {
|
||||
const labelIds = await ensureIssueLabels(target.owner, target.repo, state.selectedIssueEditDraft.labels);
|
||||
const updatedIssue = await updateGiteaIssue(target.owner, target.repo, issueNumber, {
|
||||
title: subject,
|
||||
body: state.selectedIssueEditDraft.content
|
||||
});
|
||||
const updatedOpenIssue = toOpenIssue(updatedIssue, target);
|
||||
await setGiteaIssueLabels(target.owner, target.repo, issueNumber, labelIds);
|
||||
const issueWithLabels = {
|
||||
...updatedIssue,
|
||||
labels: state.selectedIssueEditDraft.labels
|
||||
};
|
||||
const updatedOpenIssue = toOpenIssue(issueWithLabels, target);
|
||||
|
||||
set((current) => ({
|
||||
openIssues: current.openIssues.map((issue) =>
|
||||
@@ -1291,13 +1325,14 @@ export const appStore = createStore<AppState>()((set, get) => ({
|
||||
...current.selectedIssueDetail,
|
||||
issue: {
|
||||
...current.selectedIssueDetail.issue,
|
||||
...updatedIssue
|
||||
...issueWithLabels
|
||||
}
|
||||
}
|
||||
: current.selectedIssueDetail,
|
||||
selectedIssueEditDraft: {
|
||||
subject: updatedIssue.title,
|
||||
content: updatedIssue.body
|
||||
subject: issueWithLabels.title,
|
||||
content: issueWithLabels.body,
|
||||
labels: normalizeIssueLabels(issueWithLabels.labels)
|
||||
}
|
||||
}));
|
||||
|
||||
@@ -1310,31 +1345,6 @@ export const appStore = createStore<AppState>()((set, get) => ({
|
||||
set({ isUpdatingIssue: false });
|
||||
}
|
||||
},
|
||||
markIssueReady: async (issueNumber, repoFullName) => {
|
||||
const target = getIssueTarget(appStore.getState(), repoFullName);
|
||||
|
||||
if (!target) {
|
||||
setStatus(set, "Open a known URL with a linked Gitea repo before updating an issue.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
set({ isUpdatingIssue: true });
|
||||
setStatus(set, `Applying ready label to issue #${issueNumber}...`, "neutral", { autoClear: false });
|
||||
|
||||
try {
|
||||
const readyLabel = await ensureGiteaLabel(target.owner, target.repo, "ready");
|
||||
await addGiteaIssueLabels(target.owner, target.repo, issueNumber, [readyLabel.id]);
|
||||
await appStore.getState().loadOpenIssues(true);
|
||||
await appStore.getState().loadTargetIssues(true);
|
||||
await appStore.getState().loadSelectedIssueDetail(true);
|
||||
setStatus(set, `Applied ready label to issue #${issueNumber}.`, "success");
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Could not apply the ready label.";
|
||||
setStatus(set, message, "error");
|
||||
} finally {
|
||||
set({ isUpdatingIssue: false });
|
||||
}
|
||||
},
|
||||
deleteIssue: async (issueNumber, repoFullName) => {
|
||||
const target = getIssueTarget(appStore.getState(), repoFullName);
|
||||
|
||||
@@ -1373,7 +1383,8 @@ export const appStore = createStore<AppState>()((set, get) => ({
|
||||
state.selectedIssueNumber === issueNumber && state.selectedIssueRepoFullName === target.fullName
|
||||
? {
|
||||
subject: "",
|
||||
content: ""
|
||||
content: "",
|
||||
labels: []
|
||||
}
|
||||
: state.selectedIssueEditDraft,
|
||||
issueCommentDraft:
|
||||
@@ -1551,13 +1562,13 @@ export const appStore = createStore<AppState>()((set, get) => ({
|
||||
setStatus(set, "Creating Gitea issue...", "neutral", { autoClear: false });
|
||||
|
||||
try {
|
||||
const changeRequestLabel = await ensureGiteaLabel(target.owner, target.repo, "Change Request", "1d76db");
|
||||
const labelIds = await ensureIssueLabels(target.owner, target.repo, state.issueDraft.labels);
|
||||
const issue = await createGiteaIssue(
|
||||
target.owner,
|
||||
target.repo,
|
||||
subject,
|
||||
buildIssueBody(state, target.knownUrl),
|
||||
[changeRequestLabel.id]
|
||||
labelIds
|
||||
);
|
||||
const agent = getAgentById(state.issueDraft.agentId, state.agents);
|
||||
const uploads = [
|
||||
@@ -1579,17 +1590,12 @@ export const appStore = createStore<AppState>()((set, get) => ({
|
||||
await uploadGiteaIssueAttachment(target.owner, target.repo, issue.number, upload.blob, upload.name);
|
||||
}
|
||||
|
||||
if (state.issueDraft.ready) {
|
||||
const readyLabel = await ensureGiteaLabel(target.owner, target.repo, "ready");
|
||||
await addGiteaIssueLabels(target.owner, target.repo, issue.number, [readyLabel.id]);
|
||||
}
|
||||
|
||||
set({
|
||||
issueDraft: {
|
||||
subject: "",
|
||||
content: "",
|
||||
agentId: appStore.getState().agents[0]?.id ?? AVAILABLE_AGENTS[0].id,
|
||||
ready: false,
|
||||
labels: ["Change Request"],
|
||||
screenshots: [],
|
||||
files: []
|
||||
},
|
||||
@@ -1656,7 +1662,8 @@ export const appStore = createStore<AppState>()((set, get) => ({
|
||||
activeBaseUrl !== currentBaseUrl && get().openIssuesLimitToCurrentRepo
|
||||
? {
|
||||
subject: "",
|
||||
content: ""
|
||||
content: "",
|
||||
labels: []
|
||||
}
|
||||
: get().selectedIssueEditDraft
|
||||
});
|
||||
|
||||
+25
-1
@@ -446,6 +446,30 @@ fieldset {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.label-fieldset {
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.label-option-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 3px 7px;
|
||||
}
|
||||
|
||||
.label-option {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.label-option span {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: #334e68;
|
||||
font-size: 11px;
|
||||
font-weight: 750;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.radio-row {
|
||||
display: flex;
|
||||
grid-template-columns: auto 1fr;
|
||||
@@ -512,7 +536,7 @@ input[readonly] {
|
||||
|
||||
.issue-row {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
width: 100%;
|
||||
|
||||
Reference in New Issue
Block a user