@@ -44,12 +44,14 @@ 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, Issues, and Add Issue.
|
||||
- The side panel has icon menu sections for Add URL, URLs, Gitea repos, Workspaces, Prompts, Issues, Add Issue, and Agents. Agents must stay as the far-right menu item.
|
||||
- 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.
|
||||
- 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}}`. It must tell downstream agents to close the Gitea issue after the implementation has been pushed, and to leave a Gitea issue comment when delivery is blocked, partial, or risky.
|
||||
- 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.
|
||||
- The Prompts view manages workspace-scoped default prompts persisted in `chrome.storage.local` under `silmaAide.prompts`. Prompt records must include a name, workspace path, and content.
|
||||
- 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 a workspace prompt above Content, 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.
|
||||
- The Add Issue default prompt dropdown must only show prompts attached to the active URL's workspace path. Selecting a prompt copies its content into the editable Content field for human adjustment.
|
||||
- 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.
|
||||
- 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.
|
||||
@@ -63,6 +65,7 @@ Use `.env.local` for local secrets. Keep `.env.local` and any real token-bearing
|
||||
- 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 `<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.
|
||||
- Gitea API errors should include the failed operation and, for network-level failures, the configured Gitea API base URL plus actionable checks for env, extension host permissions, network/VPN access, token scope, and attachment size.
|
||||
|
||||
## 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, 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. The default global agent tells downstream agents to close the Gitea issue after pushing the implementation and to comment when delivery is blocked, partial, or risky. 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.
|
||||
The app opens in the Chrome side panel. The top icon menu includes Add URL, URLs, Gitea repos, Workspaces, Prompts, Issues, Add Issue, and Agents, with Agents pinned to the far right. 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 Prompts view manages workspace-scoped default prompts in `chrome.storage.local`; Add Issue shows matching prompts above Content and copies the selected prompt into the editable content field. 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. The default global agent tells downstream agents to close the Gitea issue after pushing the implementation and to comment when delivery is blocked, partial, or risky. 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. Gitea API errors include the failed operation and network-level failures identify the configured API base plus common checks.
|
||||
|
||||
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.
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ 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 show a Default prompt dropdown immediately above Content. It should only list prompts whose workspace path matches the active known URL and should copy the selected prompt content into the editable Content field.
|
||||
- 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 default `GLOBAL AGENT` rules must instruct downstream agents to close the Gitea issue after pushing the implementation and to leave a Gitea issue comment when delivery is blocked, partial, or risky.
|
||||
- The Add Issue view intentionally omits the redundant view header title/description.
|
||||
@@ -68,7 +69,18 @@ Required variables:
|
||||
- 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, Issues comments, or Agents editor fields must not blur the focused control.
|
||||
- Typing in Add Issue, selected issue edit fields, Issues comments, Agents editor fields, or Prompts editor fields must not blur the focused control.
|
||||
|
||||
## Prompt Management
|
||||
|
||||
- The Prompts menu item is workspace-scoped and stores prompt records in `silmaAide.prompts`.
|
||||
- The Prompts view must allow adding, editing, and deleting prompts with a name, workspace path, and content.
|
||||
- Agents must remain the far-right toolbar item when new menu items are added.
|
||||
|
||||
## Gitea Error Reporting
|
||||
|
||||
- Gitea API failures should surface the specific operation that failed, such as issue creation, label loading, attachment upload, comment creation, or repo loading.
|
||||
- Network-level Gitea failures should include the configured Gitea API base URL and mention checks for `VITE_GITEA_BASE_URL`, Chrome extension host permissions, network/VPN access, token scope, and attachment size.
|
||||
|
||||
## URL Management
|
||||
|
||||
|
||||
+7
-3
@@ -26,9 +26,9 @@
|
||||
<i data-lucide="folder-git-2"></i>
|
||||
<span>Workspaces</span>
|
||||
</button>
|
||||
<button class="tool-button" type="button" data-view="agents" title="Agents">
|
||||
<i data-lucide="bot"></i>
|
||||
<span>Agents</span>
|
||||
<button class="tool-button" type="button" data-view="prompts" title="Prompts">
|
||||
<i data-lucide="file-text"></i>
|
||||
<span>Prompts</span>
|
||||
</button>
|
||||
<button class="tool-button" type="button" data-view="open-issues" title="Issues">
|
||||
<i data-lucide="list-checks"></i>
|
||||
@@ -38,6 +38,10 @@
|
||||
<i data-lucide="circle-plus"></i>
|
||||
<span>Add Issue</span>
|
||||
</button>
|
||||
<button class="tool-button" type="button" data-view="agents" title="Agents">
|
||||
<i data-lucide="bot"></i>
|
||||
<span>Agents</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<section class="content-panel">
|
||||
|
||||
@@ -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, 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. The default global agent tells downstream agents to close the Gitea issue after pushing the implementation and to leave a Gitea issue comment when delivery is blocked, partial, or risky. 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. 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 status multiselect filtering, has an empty-by-default label filter, and shows selected issue subject/content/label edit fields that save against the selected issue repo with Gitea content-version readback verification, a direct Open in Gitea link, a placeholder Send to Codex console log action, attachments, comment chains, direct selected-issue screenshot/file attachment uploads, and a comment form with text, screenshots, and file attachments. Clicking the selected issue row again or the detail close control collapses the detail panel. 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, 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, Prompts, Issues, Add Issue, and Agents; Agents must stay at the far right. 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 Prompts view manages workspace-scoped default prompts in `chrome.storage.local` under `silmaAide.prompts`; Add Issue shows prompts matching the active URL workspace above Content and copies the selected prompt into the editable Content field for human adjustment. 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. The default global agent tells downstream agents to close the Gitea issue after pushing the implementation and to leave a Gitea issue comment when delivery is blocked, partial, or risky. 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. 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 status multiselect filtering, has an empty-by-default label filter, and shows selected issue subject/content/label edit fields that save against the selected issue repo with Gitea content-version readback verification, a direct Open in Gitea link, a placeholder Send to Codex console log action, attachments, comment chains, direct selected-issue screenshot/file attachment uploads, and a comment form with text, screenshots, and file attachments. Clicking the selected issue row again or the detail close control collapses the detail panel. 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, Issues comment, Agents editor controls, or Prompts editor controls. Gitea API errors include the failed operation and network-level failures identify the configured API base with env, permissions, network/VPN, token scope, and attachment-size checks. 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`.
|
||||
|
||||
|
||||
+10
-3
@@ -43,7 +43,7 @@
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
"name": "Agents",
|
||||
"name": "Prompts",
|
||||
"enabled": true
|
||||
},
|
||||
{
|
||||
@@ -53,15 +53,22 @@
|
||||
{
|
||||
"name": "Add Issue",
|
||||
"enabled": "when active tab base URL is known and linked to a Gitea repo"
|
||||
},
|
||||
{
|
||||
"name": "Agents",
|
||||
"enabled": true,
|
||||
"position": "far right"
|
||||
}
|
||||
],
|
||||
"repo_ordering": "Gitea repos are ordered by latest default-branch commit timestamp.",
|
||||
"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. The default global agent tells downstream agents to close the Gitea issue after pushing the implementation and to leave a Gitea issue comment when delivery is blocked, partial, or risky; persisted built-in global agents should pick up these required completion rules without replacing unrelated custom text.",
|
||||
"prompts_behavior": "The Prompts view manages workspace-scoped prompt records persisted in chrome.storage.local under silmaAide.prompts. Each prompt has a name, workspace path, and content. Add Issue shows prompts matching the active known URL's workspace path immediately above Content, and selecting one copies prompt content into the editable Content field for human adjustment.",
|
||||
"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 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.",
|
||||
"add_issue_behavior": "The form shows the target repo and active URL for the active known URL, lets the user select a workspace prompt, 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": "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. Status filtering uses Open and Closed checkboxes as a multiselect with Open checked by default. Label filtering defaults to empty and filters visible rows by a selected known label.",
|
||||
"issue_comment_behavior": "Issues owns existing issue follow-up. Selecting an issue shows a direct Open in Gitea link, a placeholder Send to Codex button that logs the selected issue payload, editable issue subject/content/label fields that save against the selected issue repo with Gitea content-version readback verification and through Gitea's issue labels endpoint, issue attachments, direct attachment upload controls, 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. Clicking the selected issue row again or the detail close control collapses the detail panel. Existing issue attachments can be deleted, and additional selected-issue screenshots/files can be uploaded directly as issue assets without creating a comment. Issue rows can delete the issue after confirmation. Draft text-entry updates must not blur focused Add Issue, selected issue edit, Issues comment, or Agents editor controls.",
|
||||
"issue_comment_behavior": "Issues owns existing issue follow-up. Selecting an issue shows a direct Open in Gitea link, a placeholder Send to Codex button that logs the selected issue payload, editable issue subject/content/label fields that save against the selected issue repo with Gitea content-version readback verification and through Gitea's issue labels endpoint, issue attachments, direct attachment upload controls, 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. Clicking the selected issue row again or the detail close control collapses the detail panel. Existing issue attachments can be deleted, and additional selected-issue screenshots/files can be uploaded directly as issue assets without creating a comment. Issue rows can delete the issue after confirmation. Draft text-entry updates must not blur focused Add Issue, selected issue edit, Issues comment, Agents editor controls, or Prompts editor controls.",
|
||||
"gitea_error_behavior": "Gitea API failures should include the failed operation. Network-level failures identify the configured Gitea API base URL and point to checks for VITE_GITEA_BASE_URL, Chrome extension host permissions, network/VPN access, token scope, and attachment size.",
|
||||
"known_urls": [
|
||||
{
|
||||
"url": "http://100.66.226.22:23030/",
|
||||
|
||||
@@ -26,18 +26,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "aarete-unified",
|
||||
"path": "/Users/Tabitha/.openclaw/workspace_Father/aarete-unified",
|
||||
"linkedRepos": [
|
||||
{
|
||||
"worktreePath": "/Users/Tabitha/.openclaw/workspace_Father/aarete-unified",
|
||||
"worktreeRelativePath": "aarete-unified",
|
||||
"fullName": "jacob-mathison/aarete-doczyai-unified",
|
||||
"webUrl": "http://gitea.orson.tealthrone/jacob-mathison/aarete-doczyai-unified"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "banner-and-pyre",
|
||||
"path": "/Users/Tabitha/.openclaw/workspace_Father/banner-and-pyre",
|
||||
@@ -98,11 +86,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "tuono",
|
||||
"path": "/Users/Tabitha/.openclaw/workspace_Father/tuono",
|
||||
"linkedRepos": []
|
||||
},
|
||||
{
|
||||
"name": "workspace_Data",
|
||||
"path": "/Users/Tabitha/.openclaw/workspace_Father/workspace_Data",
|
||||
@@ -117,20 +100,6 @@
|
||||
}
|
||||
],
|
||||
"gitWorktrees": [
|
||||
{
|
||||
"name": "aarete-unified",
|
||||
"path": "/Users/Tabitha/.openclaw/workspace_Father/aarete-unified",
|
||||
"relativePath": "aarete-unified",
|
||||
"topWorkspaceName": "aarete-unified",
|
||||
"branch": "main",
|
||||
"giteaRepo": {
|
||||
"fullName": "jacob-mathison/aarete-doczyai-unified",
|
||||
"owner": "jacob-mathison",
|
||||
"name": "aarete-doczyai-unified",
|
||||
"webUrl": "http://gitea.orson.tealthrone/jacob-mathison/aarete-doczyai-unified",
|
||||
"cloneUrl": "http://100.79.253.19:3000/jacob-mathison/aarete-doczyai-unified.git"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "automations",
|
||||
"path": "/Users/Tabitha/.openclaw/workspace_Father/aarete/automations",
|
||||
@@ -265,14 +234,6 @@
|
||||
"cloneUrl": "http://100.79.253.19:3000/jacob-mathison/mp-silma-ai-aide.git"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "tuono",
|
||||
"path": "/Users/Tabitha/.openclaw/workspace_Father/tuono",
|
||||
"relativePath": "tuono",
|
||||
"topWorkspaceName": "tuono",
|
||||
"branch": "main",
|
||||
"giteaRepo": null
|
||||
},
|
||||
{
|
||||
"name": "workspace_Data",
|
||||
"path": "/Users/Tabitha/.openclaw/workspace_Father/workspace_Data",
|
||||
|
||||
+190
-140
@@ -192,6 +192,7 @@ function getGiteaErrorMessage(error: unknown, fallback: string): string {
|
||||
|
||||
const status = error.response?.status;
|
||||
const data = error.response?.data;
|
||||
const apiBaseUrl = giteaConfig.baseUrl ? getApiBaseUrl(giteaConfig.baseUrl) : "the configured Gitea API";
|
||||
const detail =
|
||||
typeof data === "string"
|
||||
? data
|
||||
@@ -201,7 +202,12 @@ function getGiteaErrorMessage(error: unknown, fallback: string): string {
|
||||
? data.error
|
||||
: error.message;
|
||||
|
||||
return status ? `${fallback} Gitea returned ${status}: ${detail}` : `${fallback} ${detail}`;
|
||||
if (status) {
|
||||
return `${fallback} Gitea returned ${status}: ${detail}`;
|
||||
}
|
||||
|
||||
const reason = error.code ? `${detail} (${error.code})` : detail;
|
||||
return `${fallback} Could not reach ${apiBaseUrl}: ${reason}. Check VITE_GITEA_BASE_URL, Chrome extension host permissions, network/VPN access, token scope, and attachment size.`;
|
||||
}
|
||||
|
||||
function getLabelName(label: unknown): string {
|
||||
@@ -218,8 +224,12 @@ function normalizeIssueText(value: string): string {
|
||||
|
||||
export async function getCurrentGiteaUser(): Promise<GiteaUser> {
|
||||
const client = createGiteaClient();
|
||||
const response = await client.get<GiteaUser>("/user");
|
||||
return response.data;
|
||||
try {
|
||||
const response = await client.get<GiteaUser>("/user");
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(getGiteaErrorMessage(error, "Could not load the current Gitea user."));
|
||||
}
|
||||
}
|
||||
|
||||
export async function createGiteaIssue(
|
||||
@@ -230,21 +240,25 @@ export async function createGiteaIssue(
|
||||
labelIds: number[] = []
|
||||
): Promise<CreatedGiteaIssue> {
|
||||
const client = createGiteaClient();
|
||||
const response = await client.post<GiteaIssueResponse>(`/repos/${owner}/${repo}/issues`, {
|
||||
title,
|
||||
body,
|
||||
labels: labelIds
|
||||
});
|
||||
try {
|
||||
const response = await client.post<GiteaIssueResponse>(`/repos/${owner}/${repo}/issues`, {
|
||||
title,
|
||||
body,
|
||||
labels: labelIds
|
||||
});
|
||||
|
||||
const number = response.data.number;
|
||||
if (!number) {
|
||||
throw new Error("Gitea did not return an issue number.");
|
||||
const number = response.data.number;
|
||||
if (!number) {
|
||||
throw new Error("Gitea did not return an issue number.");
|
||||
}
|
||||
|
||||
return {
|
||||
number,
|
||||
webUrl: response.data.html_url || `${giteaConfig.baseUrl.replace(/\/+$/, "")}/${owner}/${repo}/issues/${number}`
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(getGiteaErrorMessage(error, `Could not create a Gitea issue in ${owner}/${repo}.`));
|
||||
}
|
||||
|
||||
return {
|
||||
number,
|
||||
webUrl: response.data.html_url || `${giteaConfig.baseUrl.replace(/\/+$/, "")}/${owner}/${repo}/issues/${number}`
|
||||
};
|
||||
}
|
||||
|
||||
export async function updateGiteaIssue(
|
||||
@@ -318,63 +332,71 @@ export async function getGiteaIssues(
|
||||
options: { limit?: number; sort?: "created" | "updated"; order?: "asc" | "desc"; state?: GiteaIssueStateFilter } = {}
|
||||
): Promise<GiteaIssueSummary[]> {
|
||||
const client = createGiteaClient();
|
||||
const response = await client.get<GiteaIssueResponse[]>(`/repos/${owner}/${repo}/issues`, {
|
||||
params: {
|
||||
limit: options.limit ?? 25,
|
||||
order: options.order ?? "desc",
|
||||
page: 1,
|
||||
sort: options.sort ?? "created",
|
||||
state: options.state ?? "all",
|
||||
type: "issues"
|
||||
}
|
||||
});
|
||||
try {
|
||||
const response = await client.get<GiteaIssueResponse[]>(`/repos/${owner}/${repo}/issues`, {
|
||||
params: {
|
||||
limit: options.limit ?? 25,
|
||||
order: options.order ?? "desc",
|
||||
page: 1,
|
||||
sort: options.sort ?? "created",
|
||||
state: options.state ?? "all",
|
||||
type: "issues"
|
||||
}
|
||||
});
|
||||
|
||||
return response.data
|
||||
.filter((issue) => issue.number !== undefined)
|
||||
.map((issue) => toIssueSummary(issue, owner, repo, issue.number));
|
||||
return response.data
|
||||
.filter((issue) => issue.number !== undefined)
|
||||
.map((issue) => toIssueSummary(issue, owner, repo, issue.number));
|
||||
} catch (error) {
|
||||
throw new Error(getGiteaErrorMessage(error, `Could not load Gitea issues for ${owner}/${repo}.`));
|
||||
}
|
||||
}
|
||||
|
||||
export async function getGiteaIssueDetail(owner: string, repo: string, issueNumber: number): Promise<GiteaIssueDetail> {
|
||||
const client = createGiteaClient();
|
||||
const [issueResponse, commentsResponse, attachmentsResponse] = await Promise.all([
|
||||
client.get<GiteaIssueResponse>(`/repos/${owner}/${repo}/issues/${issueNumber}`),
|
||||
client.get<GiteaIssueCommentResponse[]>(`/repos/${owner}/${repo}/issues/${issueNumber}/comments`, {
|
||||
params: {
|
||||
limit: 100,
|
||||
page: 1
|
||||
}
|
||||
}),
|
||||
client.get<GiteaAttachmentResponse[]>(`/repos/${owner}/${repo}/issues/${issueNumber}/assets`, {
|
||||
params: {
|
||||
limit: 100,
|
||||
page: 1
|
||||
}
|
||||
})
|
||||
]);
|
||||
try {
|
||||
const [issueResponse, commentsResponse, attachmentsResponse] = await Promise.all([
|
||||
client.get<GiteaIssueResponse>(`/repos/${owner}/${repo}/issues/${issueNumber}`),
|
||||
client.get<GiteaIssueCommentResponse[]>(`/repos/${owner}/${repo}/issues/${issueNumber}/comments`, {
|
||||
params: {
|
||||
limit: 100,
|
||||
page: 1
|
||||
}
|
||||
}),
|
||||
client.get<GiteaAttachmentResponse[]>(`/repos/${owner}/${repo}/issues/${issueNumber}/assets`, {
|
||||
params: {
|
||||
limit: 100,
|
||||
page: 1
|
||||
}
|
||||
})
|
||||
]);
|
||||
|
||||
const issue = issueResponse.data;
|
||||
const issue = issueResponse.data;
|
||||
|
||||
return {
|
||||
issue: {
|
||||
...toIssueSummary(issue, owner, repo, issueNumber),
|
||||
comments: issue.comments ?? commentsResponse.data.length
|
||||
},
|
||||
comments: commentsResponse.data.map((comment) => ({
|
||||
id: comment.id ?? 0,
|
||||
body: comment.body || "",
|
||||
webUrl: comment.html_url || "",
|
||||
createdAt: comment.created_at || "",
|
||||
updatedAt: comment.updated_at || "",
|
||||
author: comment.user?.login || "unknown"
|
||||
})),
|
||||
attachments: attachmentsResponse.data.map((attachment) => ({
|
||||
id: attachment.id ?? 0,
|
||||
name: attachment.name || "attachment",
|
||||
downloadUrl: attachment.browser_download_url || attachment.download_url || "",
|
||||
size: attachment.size ?? 0,
|
||||
createdAt: attachment.created_at || ""
|
||||
}))
|
||||
};
|
||||
return {
|
||||
issue: {
|
||||
...toIssueSummary(issue, owner, repo, issueNumber),
|
||||
comments: issue.comments ?? commentsResponse.data.length
|
||||
},
|
||||
comments: commentsResponse.data.map((comment) => ({
|
||||
id: comment.id ?? 0,
|
||||
body: comment.body || "",
|
||||
webUrl: comment.html_url || "",
|
||||
createdAt: comment.created_at || "",
|
||||
updatedAt: comment.updated_at || "",
|
||||
author: comment.user?.login || "unknown"
|
||||
})),
|
||||
attachments: attachmentsResponse.data.map((attachment) => ({
|
||||
id: attachment.id ?? 0,
|
||||
name: attachment.name || "attachment",
|
||||
downloadUrl: attachment.browser_download_url || attachment.download_url || "",
|
||||
size: attachment.size ?? 0,
|
||||
createdAt: attachment.created_at || ""
|
||||
}))
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(getGiteaErrorMessage(error, `Could not load Gitea issue #${issueNumber} in ${owner}/${repo}.`));
|
||||
}
|
||||
}
|
||||
|
||||
export async function uploadGiteaIssueAttachment(
|
||||
@@ -388,18 +410,24 @@ export async function uploadGiteaIssueAttachment(
|
||||
const formData = new FormData();
|
||||
formData.append("attachment", file, fileName);
|
||||
|
||||
const response = await client.post<GiteaAttachmentResponse>(
|
||||
`/repos/${owner}/${repo}/issues/${issueNumber}/assets`,
|
||||
formData
|
||||
);
|
||||
try {
|
||||
const response = await client.post<GiteaAttachmentResponse>(
|
||||
`/repos/${owner}/${repo}/issues/${issueNumber}/assets`,
|
||||
formData
|
||||
);
|
||||
|
||||
return {
|
||||
id: response.data.id ?? 0,
|
||||
name: response.data.name || fileName,
|
||||
downloadUrl: response.data.browser_download_url || response.data.download_url || "",
|
||||
size: response.data.size ?? file.size,
|
||||
createdAt: response.data.created_at || ""
|
||||
};
|
||||
return {
|
||||
id: response.data.id ?? 0,
|
||||
name: response.data.name || fileName,
|
||||
downloadUrl: response.data.browser_download_url || response.data.download_url || "",
|
||||
size: response.data.size ?? file.size,
|
||||
createdAt: response.data.created_at || ""
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
getGiteaErrorMessage(error, `Could not upload ${fileName} to Gitea issue #${issueNumber} in ${owner}/${repo}.`)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteGiteaIssueAttachment(
|
||||
@@ -409,7 +437,13 @@ export async function deleteGiteaIssueAttachment(
|
||||
attachmentId: number
|
||||
): Promise<void> {
|
||||
const client = createGiteaClient();
|
||||
await client.delete(`/repos/${owner}/${repo}/issues/${issueNumber}/assets/${attachmentId}`);
|
||||
try {
|
||||
await client.delete(`/repos/${owner}/${repo}/issues/${issueNumber}/assets/${attachmentId}`);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
getGiteaErrorMessage(error, `Could not delete attachment ${attachmentId} from Gitea issue #${issueNumber}.`)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function createGiteaIssueComment(
|
||||
@@ -419,24 +453,36 @@ export async function createGiteaIssueComment(
|
||||
body: string
|
||||
): Promise<void> {
|
||||
const client = createGiteaClient();
|
||||
await client.post(`/repos/${owner}/${repo}/issues/${issueNumber}/comments`, {
|
||||
body
|
||||
});
|
||||
try {
|
||||
await client.post(`/repos/${owner}/${repo}/issues/${issueNumber}/comments`, {
|
||||
body
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(getGiteaErrorMessage(error, `Could not create a comment on Gitea issue #${issueNumber}.`));
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteGiteaIssue(owner: string, repo: string, issueNumber: number): Promise<void> {
|
||||
const client = createGiteaClient();
|
||||
await client.delete(`/repos/${owner}/${repo}/issues/${issueNumber}`);
|
||||
try {
|
||||
await client.delete(`/repos/${owner}/${repo}/issues/${issueNumber}`);
|
||||
} catch (error) {
|
||||
throw new Error(getGiteaErrorMessage(error, `Could not delete Gitea issue #${issueNumber} in ${owner}/${repo}.`));
|
||||
}
|
||||
}
|
||||
|
||||
async function getGiteaLabels(owner: string, repo: string): Promise<GiteaLabelResponse[]> {
|
||||
const client = createGiteaClient();
|
||||
const response = await client.get<GiteaLabelResponse[]>(`/repos/${owner}/${repo}/labels`, {
|
||||
params: {
|
||||
limit: 100
|
||||
}
|
||||
});
|
||||
return response.data;
|
||||
try {
|
||||
const response = await client.get<GiteaLabelResponse[]>(`/repos/${owner}/${repo}/labels`, {
|
||||
params: {
|
||||
limit: 100
|
||||
}
|
||||
});
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
throw new Error(getGiteaErrorMessage(error, `Could not load Gitea labels for ${owner}/${repo}.`));
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureGiteaLabel(owner: string, repo: string, name: string, color = "0e8a16"): Promise<GiteaLabelResponse> {
|
||||
@@ -453,17 +499,19 @@ export async function ensureGiteaLabel(owner: string, repo: string, name: string
|
||||
color
|
||||
});
|
||||
return response.data;
|
||||
} catch {
|
||||
} catch (error) {
|
||||
const refreshedLabels = await getGiteaLabels(owner, repo);
|
||||
const refreshed = refreshedLabels.find((label) => label.name.toLowerCase() === name.toLowerCase());
|
||||
if (refreshed) {
|
||||
return refreshed;
|
||||
}
|
||||
throw new Error(`Could not create or find the ${name} label.`);
|
||||
throw new Error(getGiteaErrorMessage(error, `Could not create or find the ${name} label in ${owner}/${repo}.`));
|
||||
}
|
||||
}
|
||||
|
||||
export async function getReposOrderedByLastCommit(): Promise<GiteaRepoSummary[]> {
|
||||
assertGiteaConfigured();
|
||||
|
||||
const client = axios.create({
|
||||
baseURL: getApiBaseUrl(giteaConfig.baseUrl),
|
||||
headers: {
|
||||
@@ -471,62 +519,64 @@ export async function getReposOrderedByLastCommit(): Promise<GiteaRepoSummary[]>
|
||||
}
|
||||
});
|
||||
|
||||
assertGiteaConfigured();
|
||||
|
||||
const repos: GiteaRepoResponse[] = [];
|
||||
let page = 1;
|
||||
|
||||
while (page <= 10) {
|
||||
const response = await client.get<GiteaRepoResponse[]>("/user/repos", {
|
||||
params: {
|
||||
limit: 50,
|
||||
page
|
||||
try {
|
||||
while (page <= 10) {
|
||||
const response = await client.get<GiteaRepoResponse[]>("/user/repos", {
|
||||
params: {
|
||||
limit: 50,
|
||||
page
|
||||
}
|
||||
});
|
||||
|
||||
repos.push(...response.data);
|
||||
|
||||
if (response.data.length < 50) {
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
repos.push(...response.data);
|
||||
|
||||
if (response.data.length < 50) {
|
||||
break;
|
||||
page += 1;
|
||||
}
|
||||
|
||||
page += 1;
|
||||
const repoDetails = await Promise.all(
|
||||
repos.map(async (repo): Promise<GiteaRepoSummary> => {
|
||||
const owner = repo.owner?.login || giteaConfig.repoOwner;
|
||||
const defaultBranch = repo.default_branch || "main";
|
||||
const fullName = repo.full_name || `${owner}/${repo.name}`;
|
||||
|
||||
let branch: GiteaBranchResponse | null = null;
|
||||
try {
|
||||
const branchResponse = await client.get<GiteaBranchResponse>(
|
||||
`/repos/${owner}/${repo.name}/branches/${defaultBranch}`
|
||||
);
|
||||
branch = branchResponse.data;
|
||||
} catch {
|
||||
branch = null;
|
||||
}
|
||||
|
||||
return {
|
||||
name: repo.name,
|
||||
fullName,
|
||||
description: repo.description || "",
|
||||
webUrl: repo.html_url || `${giteaConfig.baseUrl.replace(/\/+$/, "")}/${fullName}`,
|
||||
cloneUrl: repo.clone_url || "",
|
||||
defaultBranch,
|
||||
isPrivate: Boolean(repo.private),
|
||||
lastCommitAt: branch?.commit?.timestamp || repo.updated_at || "",
|
||||
lastCommitSha: branch?.commit?.id || "",
|
||||
lastCommitMessage: branch?.commit?.message?.split("\n")[0] || "No commit message available"
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return repoDetails.sort((a, b) => {
|
||||
const aTime = a.lastCommitAt ? Date.parse(a.lastCommitAt) : 0;
|
||||
const bTime = b.lastCommitAt ? Date.parse(b.lastCommitAt) : 0;
|
||||
return bTime - aTime || a.fullName.localeCompare(b.fullName);
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(getGiteaErrorMessage(error, "Could not load Gitea repositories."));
|
||||
}
|
||||
|
||||
const repoDetails = await Promise.all(
|
||||
repos.map(async (repo): Promise<GiteaRepoSummary> => {
|
||||
const owner = repo.owner?.login || giteaConfig.repoOwner;
|
||||
const defaultBranch = repo.default_branch || "main";
|
||||
const fullName = repo.full_name || `${owner}/${repo.name}`;
|
||||
|
||||
let branch: GiteaBranchResponse | null = null;
|
||||
try {
|
||||
const branchResponse = await client.get<GiteaBranchResponse>(
|
||||
`/repos/${owner}/${repo.name}/branches/${defaultBranch}`
|
||||
);
|
||||
branch = branchResponse.data;
|
||||
} catch {
|
||||
branch = null;
|
||||
}
|
||||
|
||||
return {
|
||||
name: repo.name,
|
||||
fullName,
|
||||
description: repo.description || "",
|
||||
webUrl: repo.html_url || `${giteaConfig.baseUrl.replace(/\/+$/, "")}/${fullName}`,
|
||||
cloneUrl: repo.clone_url || "",
|
||||
defaultBranch,
|
||||
isPrivate: Boolean(repo.private),
|
||||
lastCommitAt: branch?.commit?.timestamp || repo.updated_at || "",
|
||||
lastCommitSha: branch?.commit?.id || "",
|
||||
lastCommitMessage: branch?.commit?.message?.split("\n")[0] || "No commit message available"
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return repoDetails.sort((a, b) => {
|
||||
const aTime = a.lastCommitAt ? Date.parse(a.lastCommitAt) : 0;
|
||||
const bTime = b.lastCommitAt ? Date.parse(b.lastCommitAt) : 0;
|
||||
return bTime - aTime || a.fullName.localeCompare(b.fullName);
|
||||
});
|
||||
}
|
||||
|
||||
+161
-3
@@ -1,4 +1,17 @@
|
||||
import { createIcons, Bot, CirclePlus, FolderGit2, GitBranch, Link, List, ListChecks, RefreshCw, Send, X } from "lucide";
|
||||
import {
|
||||
createIcons,
|
||||
Bot,
|
||||
CirclePlus,
|
||||
FileText,
|
||||
FolderGit2,
|
||||
GitBranch,
|
||||
Link,
|
||||
List,
|
||||
ListChecks,
|
||||
RefreshCw,
|
||||
Send,
|
||||
X
|
||||
} from "lucide";
|
||||
import "./styles.css";
|
||||
import {
|
||||
appStore,
|
||||
@@ -11,7 +24,8 @@ import {
|
||||
type IssueLabelOption,
|
||||
type OpenIssue,
|
||||
type OpenIssuesStatusFilter,
|
||||
type ScreenshotAttachment
|
||||
type ScreenshotAttachment,
|
||||
type WorkspacePrompt
|
||||
} from "./store";
|
||||
import type { GiteaIssueAttachment, GiteaIssueComment, GiteaIssueSummary, GiteaRepoSummary } from "./api";
|
||||
import type { KnownUrl, LocalContext, LocalWorkspace } from "./localContext";
|
||||
@@ -29,6 +43,7 @@ const viewButtons = Array.from(document.querySelectorAll<HTMLButtonElement>("[da
|
||||
const lucideIcons = {
|
||||
Bot,
|
||||
CirclePlus,
|
||||
FileText,
|
||||
FolderGit2,
|
||||
GitBranch,
|
||||
Link,
|
||||
@@ -250,6 +265,76 @@ function renderAgents(agents: AgentProfile[]): string {
|
||||
`;
|
||||
}
|
||||
|
||||
function workspacePathOptions(context: LocalContext | null, selectedPath: string): string {
|
||||
const workspaces = context?.workspaces ?? [];
|
||||
|
||||
if (!workspaces.length) {
|
||||
return selectedPath
|
||||
? `<option value="${escapeHtml(selectedPath)}" selected>${escapeHtml(selectedPath)}</option>`
|
||||
: "";
|
||||
}
|
||||
|
||||
return workspaces
|
||||
.map(
|
||||
(workspace) =>
|
||||
`<option value="${escapeHtml(workspace.path)}"${selectedAttribute(workspace.path, selectedPath)}>${escapeHtml(workspace.name)} - ${escapeHtml(workspace.path)}</option>`
|
||||
)
|
||||
.join("");
|
||||
}
|
||||
|
||||
function renderPrompts(prompts: WorkspacePrompt[]): string {
|
||||
const state = appStore.getState();
|
||||
const draft = state.promptDraft;
|
||||
|
||||
return `
|
||||
<div class="compact-list">
|
||||
${
|
||||
prompts.length
|
||||
? prompts
|
||||
.map(
|
||||
(prompt) => `
|
||||
<article class="prompt-row">
|
||||
<div class="row-main">
|
||||
<h2 class="item-title">${escapeHtml(prompt.name)}</h2>
|
||||
<span class="path-text">${escapeHtml(prompt.workspacePath)}</span>
|
||||
</div>
|
||||
<div class="prompt-actions">
|
||||
<button class="secondary-button compact-button" type="button" data-edit-prompt="${prompt.id}">Edit</button>
|
||||
<button class="danger-button compact-button" type="button" data-delete-prompt="${prompt.id}">Delete</button>
|
||||
</div>
|
||||
</article>
|
||||
`
|
||||
)
|
||||
.join("")
|
||||
: `<div class="empty-state">No workspace prompts saved yet.</div>`
|
||||
}
|
||||
</div>
|
||||
<article class="item-card prompt-editor-card">
|
||||
<div class="form-stack">
|
||||
<label>
|
||||
<span>Name</span>
|
||||
<input id="prompt-name" type="text" value="${escapeHtml(draft.name)}" placeholder="Default implementation prompt" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Workspace</span>
|
||||
<select id="prompt-workspace">
|
||||
<option value="">Pick a workspace</option>
|
||||
${workspacePathOptions(state.localContext, draft.workspacePath)}
|
||||
</select>
|
||||
</label>
|
||||
<label>
|
||||
<span>Content</span>
|
||||
<textarea id="prompt-content" rows="12" placeholder="Prompt text copied into Add Issue content">${escapeHtml(draft.content)}</textarea>
|
||||
</label>
|
||||
<div class="prompt-editor-actions">
|
||||
<button id="new-prompt" class="secondary-button" type="button">New</button>
|
||||
<button id="save-prompt" class="primary-button" type="button">${state.editingPromptId ? "Save prompt" : "Add prompt"}</button>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderUrls(urls: KnownUrl[]): string {
|
||||
if (!urls.length) {
|
||||
return `<div class="empty-state">No known URLs registered.</div>`;
|
||||
@@ -404,6 +489,18 @@ function renderAgentOptions(agents: AgentProfile[], selectedAgentId: string): st
|
||||
.join("");
|
||||
}
|
||||
|
||||
function renderIssuePromptOptions(prompts: WorkspacePrompt[], selectedPromptId: string): string {
|
||||
return `
|
||||
<option value="">No default prompt</option>
|
||||
${prompts
|
||||
.map(
|
||||
(prompt) =>
|
||||
`<option value="${escapeHtml(prompt.id)}"${prompt.id === selectedPromptId ? " selected" : ""}>${escapeHtml(prompt.name)}</option>`
|
||||
)
|
||||
.join("")}
|
||||
`;
|
||||
}
|
||||
|
||||
function renderLabelOptions(selectedLabels: IssueLabelOption[], target: "draft" | "edit"): string {
|
||||
return `
|
||||
<fieldset class="label-fieldset">
|
||||
@@ -820,6 +917,8 @@ function renderAddIssue(): string {
|
||||
return `<div class="empty-state">${escapeHtml(knownUrl.label)} is linked to a workspace but not a Gitea repo.</div>`;
|
||||
}
|
||||
|
||||
const workspacePrompts = state.prompts.filter((prompt) => prompt.workspacePath === knownUrl.workspacePath);
|
||||
|
||||
return `
|
||||
<article class="item-card issue-card">
|
||||
<div class="target-summary">
|
||||
@@ -839,6 +938,13 @@ function renderAddIssue(): string {
|
||||
<input id="issue-subject" type="text" value="${escapeHtml(draft.subject)}" placeholder="Issue subject" />
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span>Default prompt</span>
|
||||
<select id="issue-prompt">
|
||||
${renderIssuePromptOptions(workspacePrompts, draft.promptId)}
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label>
|
||||
<span>Content</span>
|
||||
<textarea id="issue-content" rows="8" placeholder="Describe the issue">${escapeHtml(draft.content)}</textarea>
|
||||
@@ -937,6 +1043,50 @@ function bindViewActions(): void {
|
||||
});
|
||||
});
|
||||
|
||||
viewContent?.querySelector<HTMLInputElement>("#prompt-name")?.addEventListener("input", (event) => {
|
||||
appStore.getState().setPromptDraft({
|
||||
name: (event.target as HTMLInputElement).value
|
||||
});
|
||||
});
|
||||
|
||||
viewContent?.querySelector<HTMLSelectElement>("#prompt-workspace")?.addEventListener("change", (event) => {
|
||||
appStore.getState().setPromptDraft({
|
||||
workspacePath: (event.target as HTMLSelectElement).value
|
||||
});
|
||||
});
|
||||
|
||||
viewContent?.querySelector<HTMLTextAreaElement>("#prompt-content")?.addEventListener("input", (event) => {
|
||||
appStore.getState().setPromptDraft({
|
||||
content: (event.target as HTMLTextAreaElement).value
|
||||
});
|
||||
});
|
||||
|
||||
viewContent?.querySelector<HTMLButtonElement>("#new-prompt")?.addEventListener("click", () => {
|
||||
appStore.getState().startNewPrompt();
|
||||
});
|
||||
|
||||
viewContent?.querySelector<HTMLButtonElement>("#save-prompt")?.addEventListener("click", () => {
|
||||
void appStore.getState().savePrompt();
|
||||
});
|
||||
|
||||
viewContent?.querySelectorAll<HTMLButtonElement>("[data-edit-prompt]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const id = button.dataset.editPrompt;
|
||||
if (id) {
|
||||
appStore.getState().editPrompt(id);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
viewContent?.querySelectorAll<HTMLButtonElement>("[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<HTMLButtonElement>("[data-open-url]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const url = button.dataset.openUrl;
|
||||
@@ -1004,6 +1154,10 @@ function bindViewActions(): void {
|
||||
});
|
||||
});
|
||||
|
||||
viewContent?.querySelector<HTMLSelectElement>("#issue-prompt")?.addEventListener("change", (event) => {
|
||||
appStore.getState().setIssuePrompt((event.target as HTMLSelectElement).value);
|
||||
});
|
||||
|
||||
viewContent?.querySelector<HTMLSelectElement>("#issue-agent")?.addEventListener("change", (event) => {
|
||||
appStore.getState().setIssueDraft({
|
||||
agentId: (event.target as HTMLSelectElement).value
|
||||
@@ -1308,6 +1462,7 @@ function renderView(): void {
|
||||
"add-url": "Add URL",
|
||||
agents: "Agents",
|
||||
"open-issues": "Issues",
|
||||
prompts: "Prompts",
|
||||
urls: "URLs",
|
||||
repos: "Gitea repos",
|
||||
workspaces: "Workspaces"
|
||||
@@ -1318,6 +1473,7 @@ function renderView(): void {
|
||||
"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"}.`
|
||||
@@ -1338,6 +1494,8 @@ function renderView(): void {
|
||||
viewContent.innerHTML = renderRepos(state.repos);
|
||||
} else if (state.activeView === "workspaces") {
|
||||
viewContent.innerHTML = renderWorkspaces(context?.workspaces ?? []);
|
||||
} 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") {
|
||||
@@ -1389,7 +1547,7 @@ function shouldRenderView(): boolean {
|
||||
}
|
||||
|
||||
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, [data-label-target]"
|
||||
"#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, [data-label-target]"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+167
-3
@@ -24,7 +24,7 @@ import { loadLocalContext, type LocalContext } from "./localContext";
|
||||
import type { KnownUrl, LocalGiteaRepoLink, LocalWorkspace } from "./localContext";
|
||||
|
||||
export type StatusTone = "neutral" | "success" | "error";
|
||||
export type AppView = "urls" | "repos" | "workspaces" | "agents" | "open-issues" | "add-url" | "add-issue";
|
||||
export type AppView = "urls" | "repos" | "workspaces" | "prompts" | "agents" | "open-issues" | "add-url" | "add-issue";
|
||||
export type AddUrlMode = "workspace" | "repo";
|
||||
export type ScreenshotTarget = "issue" | "comment" | "issue-attachment";
|
||||
export type OpenIssuesStatusFilter = "open" | "closed";
|
||||
@@ -50,6 +50,13 @@ export interface AgentProfile {
|
||||
content: string;
|
||||
}
|
||||
|
||||
export interface WorkspacePrompt {
|
||||
id: string;
|
||||
name: string;
|
||||
workspacePath: string;
|
||||
content: string;
|
||||
}
|
||||
|
||||
const DEFAULT_GLOBAL_AGENT_CONTENT = `# GLOBAL AGENT
|
||||
|
||||
## Mission
|
||||
@@ -101,6 +108,13 @@ const EMPTY_AGENT_DRAFT: AgentProfile = {
|
||||
content: ""
|
||||
};
|
||||
|
||||
const EMPTY_PROMPT_DRAFT: WorkspacePrompt = {
|
||||
id: "",
|
||||
name: "",
|
||||
workspacePath: "",
|
||||
content: ""
|
||||
};
|
||||
|
||||
interface AddUrlDraft {
|
||||
mode: AddUrlMode | null;
|
||||
selectedValue: string;
|
||||
@@ -124,6 +138,7 @@ export interface OpenIssue extends GiteaIssueSummary {
|
||||
interface IssueDraft {
|
||||
subject: string;
|
||||
content: string;
|
||||
promptId: string;
|
||||
agentId: string;
|
||||
labels: IssueLabelOption[];
|
||||
screenshots: ScreenshotAttachment[];
|
||||
@@ -177,6 +192,9 @@ interface AppState {
|
||||
agents: AgentProfile[];
|
||||
agentDraft: AgentProfile;
|
||||
editingAgentId: string | null;
|
||||
prompts: WorkspacePrompt[];
|
||||
promptDraft: WorkspacePrompt;
|
||||
editingPromptId: string | null;
|
||||
screenshotPreview: ScreenshotPreview | null;
|
||||
targetIssues: GiteaIssueSummary[];
|
||||
openIssues: OpenIssue[];
|
||||
@@ -222,6 +240,12 @@ interface AppState {
|
||||
editAgent: (id: string) => void;
|
||||
saveAgent: () => Promise<void>;
|
||||
deleteAgent: (id: string) => Promise<void>;
|
||||
setPromptDraft: (draft: Partial<WorkspacePrompt>) => void;
|
||||
startNewPrompt: () => void;
|
||||
editPrompt: (id: string) => void;
|
||||
savePrompt: () => Promise<void>;
|
||||
deletePrompt: (id: string) => Promise<void>;
|
||||
setIssuePrompt: (promptId: string) => void;
|
||||
openScreenshotPreview: (target: ScreenshotTarget, id: string) => void;
|
||||
closeScreenshotPreview: () => void;
|
||||
setScreenshotPreviewCrop: (crop: Partial<CropArea>) => void;
|
||||
@@ -255,6 +279,7 @@ interface AppState {
|
||||
const USER_URLS_STORAGE_KEY = "silmaAide.userUrls";
|
||||
const DELETED_URL_BASES_STORAGE_KEY = "silmaAide.deletedUrlBases";
|
||||
const AGENTS_STORAGE_KEY = "silmaAide.agents";
|
||||
const PROMPTS_STORAGE_KEY = "silmaAide.prompts";
|
||||
let statusTimer: number | undefined;
|
||||
|
||||
function getBaseUrl(value: string): string {
|
||||
@@ -630,6 +655,29 @@ function normalizeAgents(agents: AgentProfile[]): AgentProfile[] {
|
||||
return normalized.length ? normalized : AVAILABLE_AGENTS;
|
||||
}
|
||||
|
||||
function getFallbackWorkspacePath(state: AppState): string {
|
||||
return findKnownUrlForBase(state)?.workspacePath ?? state.localContext?.workspaces[0]?.path ?? "";
|
||||
}
|
||||
|
||||
function normalizePrompt(prompt: Partial<WorkspacePrompt>, fallbackWorkspacePath: string): WorkspacePrompt {
|
||||
return {
|
||||
id: prompt.id || crypto.randomUUID(),
|
||||
name: prompt.name?.trim() || "Untitled prompt",
|
||||
workspacePath: prompt.workspacePath?.trim() || fallbackWorkspacePath,
|
||||
content: prompt.content?.trim() || ""
|
||||
};
|
||||
}
|
||||
|
||||
function normalizePrompts(prompts: WorkspacePrompt[], fallbackWorkspacePath: string): WorkspacePrompt[] {
|
||||
return prompts
|
||||
.map((prompt) => normalizePrompt(prompt, fallbackWorkspacePath))
|
||||
.filter((prompt) => prompt.name && prompt.workspacePath);
|
||||
}
|
||||
|
||||
function getPromptById(id: string, prompts: WorkspacePrompt[]): WorkspacePrompt | null {
|
||||
return prompts.find((prompt) => prompt.id === id) ?? null;
|
||||
}
|
||||
|
||||
function normalizeIssueLabels(labels: string[]): IssueLabelOption[] {
|
||||
return ISSUE_LABEL_OPTIONS.filter((option) => labels.some((label) => label.toLowerCase() === option.toLowerCase()));
|
||||
}
|
||||
@@ -794,6 +842,7 @@ export const appStore = createStore<AppState>()((set, get) => ({
|
||||
issueDraft: {
|
||||
subject: "",
|
||||
content: "",
|
||||
promptId: "",
|
||||
agentId: AVAILABLE_AGENTS[0].id,
|
||||
labels: ["Change Request"],
|
||||
screenshots: [],
|
||||
@@ -811,6 +860,9 @@ export const appStore = createStore<AppState>()((set, get) => ({
|
||||
agents: AVAILABLE_AGENTS,
|
||||
agentDraft: AVAILABLE_AGENTS[0],
|
||||
editingAgentId: AVAILABLE_AGENTS[0].id,
|
||||
prompts: [],
|
||||
promptDraft: EMPTY_PROMPT_DRAFT,
|
||||
editingPromptId: null,
|
||||
screenshotPreview: null,
|
||||
targetIssues: [],
|
||||
openIssues: [],
|
||||
@@ -1021,6 +1073,99 @@ export const appStore = createStore<AppState>()((set, get) => ({
|
||||
}));
|
||||
setStatus(set, `Deleted ${agent.name}.`, "success");
|
||||
},
|
||||
setPromptDraft: (draft) =>
|
||||
set((state) => ({
|
||||
promptDraft: {
|
||||
...state.promptDraft,
|
||||
...draft
|
||||
}
|
||||
})),
|
||||
startNewPrompt: () =>
|
||||
set((state) => ({
|
||||
promptDraft: {
|
||||
...EMPTY_PROMPT_DRAFT,
|
||||
id: crypto.randomUUID(),
|
||||
workspacePath: getFallbackWorkspacePath(state)
|
||||
},
|
||||
editingPromptId: null
|
||||
})),
|
||||
editPrompt: (id) =>
|
||||
set((state) => {
|
||||
const prompt = state.prompts.find((item) => item.id === id);
|
||||
return prompt
|
||||
? {
|
||||
promptDraft: { ...prompt },
|
||||
editingPromptId: id
|
||||
}
|
||||
: {};
|
||||
}),
|
||||
savePrompt: async () => {
|
||||
const state = appStore.getState();
|
||||
const draft = normalizePrompt(
|
||||
{
|
||||
...state.promptDraft,
|
||||
id: state.editingPromptId ?? state.promptDraft.id ?? crypto.randomUUID()
|
||||
},
|
||||
getFallbackWorkspacePath(state)
|
||||
);
|
||||
|
||||
if (!draft.name.trim() || !draft.workspacePath.trim() || !draft.content.trim()) {
|
||||
setStatus(set, "Prompt name, workspace, and content are required.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const prompts = state.editingPromptId
|
||||
? state.prompts.map((prompt) => (prompt.id === state.editingPromptId ? draft : prompt))
|
||||
: [...state.prompts, draft];
|
||||
const normalizedPrompts = normalizePrompts(prompts, getFallbackWorkspacePath(state));
|
||||
|
||||
await storageSet(PROMPTS_STORAGE_KEY, normalizedPrompts);
|
||||
set({
|
||||
prompts: normalizedPrompts,
|
||||
promptDraft: { ...draft },
|
||||
editingPromptId: draft.id
|
||||
});
|
||||
setStatus(set, `Saved ${draft.name}.`, "success");
|
||||
},
|
||||
deletePrompt: async (id) => {
|
||||
const state = appStore.getState();
|
||||
const prompt = state.prompts.find((item) => item.id === id);
|
||||
|
||||
if (!prompt) {
|
||||
setStatus(set, "Could not find that prompt.", "error");
|
||||
return;
|
||||
}
|
||||
|
||||
const prompts = state.prompts.filter((item) => item.id !== id);
|
||||
await storageSet(PROMPTS_STORAGE_KEY, prompts);
|
||||
set((current) => ({
|
||||
prompts,
|
||||
issueDraft: {
|
||||
...current.issueDraft,
|
||||
promptId: current.issueDraft.promptId === id ? "" : current.issueDraft.promptId
|
||||
},
|
||||
promptDraft: current.editingPromptId === id
|
||||
? {
|
||||
...EMPTY_PROMPT_DRAFT,
|
||||
workspacePath: getFallbackWorkspacePath(current)
|
||||
}
|
||||
: current.promptDraft,
|
||||
editingPromptId: current.editingPromptId === id ? null : current.editingPromptId
|
||||
}));
|
||||
setStatus(set, `Deleted ${prompt.name}.`, "success");
|
||||
},
|
||||
setIssuePrompt: (promptId) =>
|
||||
set((state) => {
|
||||
const prompt = promptId ? getPromptById(promptId, state.prompts) : null;
|
||||
|
||||
return {
|
||||
issueDraft: {
|
||||
...state.issueDraft,
|
||||
promptId: prompt?.id ?? "",
|
||||
content: prompt ? prompt.content : state.issueDraft.content
|
||||
}
|
||||
};
|
||||
}),
|
||||
openScreenshotPreview: (target, id) =>
|
||||
set({
|
||||
screenshotPreview: {
|
||||
@@ -1801,6 +1946,7 @@ export const appStore = createStore<AppState>()((set, get) => ({
|
||||
issueDraft: {
|
||||
subject: "",
|
||||
content: "",
|
||||
promptId: "",
|
||||
agentId: appStore.getState().agents[0]?.id ?? AVAILABLE_AGENTS[0].id,
|
||||
labels: ["Change Request"],
|
||||
screenshots: [],
|
||||
@@ -1833,6 +1979,13 @@ export const appStore = createStore<AppState>()((set, get) => ({
|
||||
activeBaseUrl !== currentBaseUrl
|
||||
? { mode: null, selectedValue: "" }
|
||||
: get().addUrlDraft,
|
||||
issueDraft:
|
||||
activeBaseUrl !== currentBaseUrl
|
||||
? {
|
||||
...get().issueDraft,
|
||||
promptId: ""
|
||||
}
|
||||
: get().issueDraft,
|
||||
targetIssues:
|
||||
activeBaseUrl !== currentBaseUrl
|
||||
? []
|
||||
@@ -1882,16 +2035,18 @@ export const appStore = createStore<AppState>()((set, get) => ({
|
||||
setStatus(set, "Loading workspace and Gitea context...", "neutral", { autoClear: false });
|
||||
|
||||
try {
|
||||
const [localContext, repos, userUrls, deletedUrlBases, agents, activeTabUrl] = await Promise.all([
|
||||
const [localContext, repos, userUrls, deletedUrlBases, agents, prompts, activeTabUrl] = await Promise.all([
|
||||
loadLocalContext(),
|
||||
getReposOrderedByLastCommit(),
|
||||
storageGet<KnownUrl[]>(USER_URLS_STORAGE_KEY, []),
|
||||
storageGet<string[]>(DELETED_URL_BASES_STORAGE_KEY, []),
|
||||
storageGet<AgentProfile[]>(AGENTS_STORAGE_KEY, AVAILABLE_AGENTS),
|
||||
storageGet<WorkspacePrompt[]>(PROMPTS_STORAGE_KEY, []),
|
||||
getActiveTabUrl()
|
||||
]);
|
||||
const activeBaseUrl = getBaseUrl(activeTabUrl);
|
||||
const normalizedAgents = normalizeAgents(agents);
|
||||
const normalizedPrompts = normalizePrompts(prompts, localContext.workspaces[0]?.path ?? "");
|
||||
|
||||
set({
|
||||
localContext,
|
||||
@@ -1899,15 +2054,24 @@ export const appStore = createStore<AppState>()((set, get) => ({
|
||||
userUrls: userUrls.filter((url) => !deletedUrlBases.includes(getBaseUrl(url.url))),
|
||||
deletedUrlBases,
|
||||
agents: normalizedAgents,
|
||||
prompts: normalizedPrompts,
|
||||
issueDraft: {
|
||||
...get().issueDraft,
|
||||
agentId: normalizedAgents.some((agent) => agent.id === get().issueDraft.agentId)
|
||||
? get().issueDraft.agentId
|
||||
: normalizedAgents[0].id
|
||||
: normalizedAgents[0].id,
|
||||
promptId: normalizedPrompts.some((prompt) => prompt.id === get().issueDraft.promptId)
|
||||
? get().issueDraft.promptId
|
||||
: ""
|
||||
},
|
||||
agentDraft: normalizedAgents.find((agent) => agent.id === get().editingAgentId) ?? normalizedAgents[0],
|
||||
editingAgentId:
|
||||
normalizedAgents.find((agent) => agent.id === get().editingAgentId)?.id ?? normalizedAgents[0].id,
|
||||
promptDraft: normalizedPrompts.find((prompt) => prompt.id === get().editingPromptId) ?? {
|
||||
...EMPTY_PROMPT_DRAFT,
|
||||
workspacePath: localContext.workspaces[0]?.path ?? ""
|
||||
},
|
||||
editingPromptId: normalizedPrompts.find((prompt) => prompt.id === get().editingPromptId)?.id ?? null,
|
||||
activeTabUrl,
|
||||
activeBaseUrl
|
||||
});
|
||||
|
||||
+12
-6
@@ -62,7 +62,7 @@ h1 {
|
||||
|
||||
.toolbar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||
grid-template-columns: repeat(8, minmax(0, 1fr));
|
||||
gap: 1px;
|
||||
border-bottom: 1px solid #d9e2ec;
|
||||
background: #d9e2ec;
|
||||
@@ -180,7 +180,8 @@ h1 {
|
||||
|
||||
.workspace-row,
|
||||
.url-row,
|
||||
.agent-row {
|
||||
.agent-row,
|
||||
.prompt-row {
|
||||
display: grid;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
@@ -197,13 +198,15 @@ h1 {
|
||||
grid-template-columns: minmax(150px, 1fr) auto;
|
||||
}
|
||||
|
||||
.agent-row {
|
||||
.agent-row,
|
||||
.prompt-row {
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.workspace-row:first-child,
|
||||
.url-row:first-child,
|
||||
.agent-row:first-child {
|
||||
.agent-row:first-child,
|
||||
.prompt-row:first-child {
|
||||
border-top: 1px solid #d9e2ec;
|
||||
}
|
||||
|
||||
@@ -350,13 +353,16 @@ a.item-title:hover,
|
||||
}
|
||||
|
||||
.agent-actions,
|
||||
.agent-editor-actions {
|
||||
.agent-editor-actions,
|
||||
.prompt-actions,
|
||||
.prompt-editor-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.agent-editor-actions .primary-button {
|
||||
.agent-editor-actions .primary-button,
|
||||
.prompt-editor-actions .primary-button {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user