This commit is contained in:
+237
-93
@@ -29,14 +29,14 @@ import {
|
||||
type WorkspacePrompt
|
||||
} from "./store";
|
||||
import type { GiteaIssueAttachment, GiteaIssueComment, GiteaIssueSummary, GiteaRepoSummary } from "./api";
|
||||
import type { KnownUrl, LocalContext, LocalWorkspace } from "./localContext";
|
||||
import type { KnownUrl, LocalContext, LocalFolderCandidate, LocalWorkspace } from "./localContext";
|
||||
|
||||
type CropHandle = "n" | "e" | "s" | "w" | "nw" | "ne" | "sw" | "se";
|
||||
|
||||
const status = document.querySelector<HTMLElement>("#status");
|
||||
const refreshData = document.querySelector<HTMLButtonElement>("#refresh-data");
|
||||
const addUrlButton = document.querySelector<HTMLButtonElement>("#add-url");
|
||||
const addIssueButton = document.querySelector<HTMLButtonElement>("#add-issue");
|
||||
const manageViewSelect = document.querySelector<HTMLSelectElement>("#manage-view-select");
|
||||
const viewHeader = document.querySelector<HTMLElement>("#view-header");
|
||||
const viewContent = document.querySelector<HTMLElement>("#view-content");
|
||||
const viewButtons = Array.from(document.querySelectorAll<HTMLButtonElement>("[data-view]"));
|
||||
@@ -185,66 +185,49 @@ function renderRepos(repos: GiteaRepoSummary[]): string {
|
||||
function renderWorkspaces(workspaces: LocalWorkspace[]): string {
|
||||
const state = appStore.getState();
|
||||
const userWorkspacePaths = new Set(state.userWorkspaces.map((workspace) => workspace.path));
|
||||
const draft = state.workspaceDraft;
|
||||
const workspaceList = workspaces.length
|
||||
? `
|
||||
<div class="compact-list">
|
||||
${workspaces
|
||||
.map((workspace) => {
|
||||
const links = workspace.linkedRepos.length
|
||||
? workspace.linkedRepos
|
||||
.map(
|
||||
(repo) =>
|
||||
`<a class="repo-link" href="${repo.webUrl}" target="_blank" rel="noreferrer">${escapeHtml(repo.fullName)}</a>`
|
||||
)
|
||||
.join("")
|
||||
: `<span class="muted">No Gitea repo link established</span>`;
|
||||
|
||||
if (!workspaces.length) {
|
||||
return `
|
||||
<div class="empty-state">No workspaces found in workspace_Father.</div>
|
||||
${renderWorkspaceEditor(draft)}
|
||||
`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="compact-list">
|
||||
${workspaces
|
||||
.map((workspace) => {
|
||||
const links = workspace.linkedRepos.length
|
||||
? workspace.linkedRepos
|
||||
.map(
|
||||
(repo) =>
|
||||
`<a class="repo-link" href="${repo.webUrl}" target="_blank" rel="noreferrer">${escapeHtml(repo.fullName)}</a>`
|
||||
)
|
||||
.join("")
|
||||
: `<span class="muted">No Gitea repo link established</span>`;
|
||||
|
||||
return `
|
||||
<article class="workspace-row">
|
||||
<div class="row-main">
|
||||
<h2 class="item-title">${escapeHtml(workspace.name)}</h2>
|
||||
<p class="path-text">${escapeHtml(workspace.path)}</p>
|
||||
</div>
|
||||
<div class="repo-link-list">${links}</div>
|
||||
<div class="workspace-actions">
|
||||
${
|
||||
userWorkspacePaths.has(workspace.path)
|
||||
? `<button class="danger-button compact-button" type="button" data-delete-user-workspace="${escapeHtml(workspace.path)}">Delete</button>`
|
||||
: `<span class="muted">Generated</span>`
|
||||
}
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
})
|
||||
.join("")}
|
||||
</div>
|
||||
${renderWorkspaceEditor(draft)}
|
||||
`;
|
||||
}
|
||||
|
||||
function renderWorkspaceEditor(draft: { name: string; path: string }): string {
|
||||
return `
|
||||
<article class="item-card workspace-editor-card">
|
||||
<div class="form-stack">
|
||||
<label>
|
||||
<span>Name</span>
|
||||
<input id="workspace-name" type="text" value="${escapeHtml(draft.name)}" placeholder="workspace-name" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Path</span>
|
||||
<input id="workspace-path" type="text" value="${escapeHtml(draft.path)}" placeholder="/Users/Tabitha/path/to/workspace" />
|
||||
</label>
|
||||
<button id="save-workspace" class="primary-button" type="button" ${draft.path.trim() ? "" : "disabled"}>Add workspace</button>
|
||||
return `
|
||||
<article class="workspace-row">
|
||||
<div class="row-main">
|
||||
<h2 class="item-title">${escapeHtml(workspace.name)}</h2>
|
||||
<p class="path-text">${escapeHtml(workspace.path)}</p>
|
||||
</div>
|
||||
<div class="repo-link-list">${links}</div>
|
||||
<div class="workspace-actions">
|
||||
${
|
||||
userWorkspacePaths.has(workspace.path)
|
||||
? `<button class="danger-button compact-button" type="button" data-delete-user-workspace="${escapeHtml(workspace.path)}">Delete</button>`
|
||||
: `<span class="muted">Generated</span>`
|
||||
}
|
||||
</div>
|
||||
</article>
|
||||
`;
|
||||
})
|
||||
.join("")}
|
||||
</div>
|
||||
</article>
|
||||
`
|
||||
: `<div class="empty-state">No workspaces found.</div>`;
|
||||
|
||||
return `
|
||||
<div class="view-action-row">
|
||||
<button id="open-workspace-dialog" class="secondary-button compact-button" type="button">
|
||||
Add workspace
|
||||
</button>
|
||||
</div>
|
||||
${workspaceList}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -368,35 +351,45 @@ function renderPrompts(prompts: WorkspacePrompt[]): string {
|
||||
}
|
||||
|
||||
function renderUrls(urls: KnownUrl[]): string {
|
||||
if (!urls.length) {
|
||||
return `<div class="empty-state">No known URLs registered.</div>`;
|
||||
}
|
||||
const state = appStore.getState();
|
||||
const canOpenAddUrl = Boolean(state.activeBaseUrl && !isActiveBaseUrlKnown());
|
||||
const urlList = urls.length
|
||||
? `
|
||||
<div class="compact-list">
|
||||
${urls
|
||||
.map(
|
||||
(knownUrl) => `
|
||||
<article class="url-row">
|
||||
<div class="row-main">
|
||||
<h2 class="item-title">${escapeHtml(knownUrl.label)}</h2>
|
||||
<a class="url-text" href="${knownUrl.url}" target="_blank" rel="noreferrer">${escapeHtml(knownUrl.url)}</a>
|
||||
<span class="path-text">${escapeHtml(knownUrl.workspacePath)}</span>
|
||||
${
|
||||
knownUrl.giteaRepo
|
||||
? `<a class="repo-link" href="${knownUrl.giteaRepo.webUrl}" target="_blank" rel="noreferrer">${escapeHtml(knownUrl.giteaRepo.fullName)}</a>`
|
||||
: `<span class="muted">No Gitea repo link established</span>`
|
||||
}
|
||||
</div>
|
||||
<div class="url-actions">
|
||||
<button class="visit-button" type="button" data-open-url="${knownUrl.url}">Quick visit</button>
|
||||
<button class="danger-button" type="button" data-delete-url="${knownUrl.url}">Delete</button>
|
||||
</div>
|
||||
</article>
|
||||
`
|
||||
)
|
||||
.join("")}
|
||||
</div>
|
||||
`
|
||||
: `<div class="empty-state">No known URLs registered.</div>`;
|
||||
|
||||
return `
|
||||
<div class="compact-list">
|
||||
${urls
|
||||
.map(
|
||||
(knownUrl) => `
|
||||
<article class="url-row">
|
||||
<div class="row-main">
|
||||
<h2 class="item-title">${escapeHtml(knownUrl.label)}</h2>
|
||||
<a class="url-text" href="${knownUrl.url}" target="_blank" rel="noreferrer">${escapeHtml(knownUrl.url)}</a>
|
||||
<span class="path-text">${escapeHtml(knownUrl.workspacePath)}</span>
|
||||
${
|
||||
knownUrl.giteaRepo
|
||||
? `<a class="repo-link" href="${knownUrl.giteaRepo.webUrl}" target="_blank" rel="noreferrer">${escapeHtml(knownUrl.giteaRepo.fullName)}</a>`
|
||||
: `<span class="muted">No Gitea repo link established</span>`
|
||||
}
|
||||
</div>
|
||||
<div class="url-actions">
|
||||
<button class="visit-button" type="button" data-open-url="${knownUrl.url}">Quick visit</button>
|
||||
<button class="danger-button" type="button" data-delete-url="${knownUrl.url}">Delete</button>
|
||||
</div>
|
||||
</article>
|
||||
`
|
||||
)
|
||||
.join("")}
|
||||
<div class="view-action-row">
|
||||
<button id="open-add-url-dialog" class="secondary-button compact-button" type="button" ${canOpenAddUrl ? "" : "disabled"}>
|
||||
Add URL
|
||||
</button>
|
||||
<span class="muted">${escapeHtml(state.activeBaseUrl || "Open an http or https tab to add a URL.")}</span>
|
||||
</div>
|
||||
${urlList}
|
||||
`;
|
||||
}
|
||||
|
||||
@@ -484,6 +477,103 @@ function renderAddUrl(): string {
|
||||
`;
|
||||
}
|
||||
|
||||
function workspaceFolderCandidates(): LocalFolderCandidate[] {
|
||||
const state = appStore.getState();
|
||||
const workspaceCandidates = getAllWorkspaces(state).map((workspace) => ({
|
||||
name: workspace.name,
|
||||
path: workspace.path,
|
||||
source: "workspace-root" as const
|
||||
}));
|
||||
const contextCandidates = state.localContext?.folderCandidates ?? [];
|
||||
const candidates = [...workspaceCandidates, ...contextCandidates];
|
||||
|
||||
return candidates
|
||||
.filter((candidate, index) => candidates.findIndex((item) => item.path === candidate.path) === index)
|
||||
.sort((a, b) => a.path.localeCompare(b.path));
|
||||
}
|
||||
|
||||
function renderWorkspaceSearchResults(candidates: LocalFolderCandidate[], query: string): string {
|
||||
const normalizedQuery = query.trim().toLowerCase();
|
||||
const matches = (normalizedQuery
|
||||
? candidates.filter(
|
||||
(candidate) =>
|
||||
candidate.name.toLowerCase().includes(normalizedQuery) ||
|
||||
candidate.path.toLowerCase().includes(normalizedQuery)
|
||||
)
|
||||
: candidates
|
||||
).slice(0, 40);
|
||||
|
||||
if (!matches.length) {
|
||||
return `<div class="empty-state">No matching folders found in the generated folder index.</div>`;
|
||||
}
|
||||
|
||||
return `
|
||||
<div class="folder-result-list">
|
||||
${matches
|
||||
.map(
|
||||
(candidate) => `
|
||||
<button class="folder-result" type="button" data-select-workspace-folder="${escapeHtml(candidate.path)}" data-folder-name="${escapeHtml(candidate.name)}">
|
||||
<span>${escapeHtml(candidate.name)}</span>
|
||||
<span class="path-text">${escapeHtml(candidate.path)}</span>
|
||||
</button>
|
||||
`
|
||||
)
|
||||
.join("")}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderWorkspaceDialog(): string {
|
||||
const state = appStore.getState();
|
||||
const draft = state.workspaceDraft;
|
||||
|
||||
return `
|
||||
<div class="form-stack">
|
||||
<label>
|
||||
<span>Folder search</span>
|
||||
<input id="workspace-search" type="text" value="${escapeHtml(draft.search)}" placeholder="Search folders by name or path" />
|
||||
</label>
|
||||
<div id="workspace-folder-results">
|
||||
${renderWorkspaceSearchResults(workspaceFolderCandidates(), draft.search)}
|
||||
</div>
|
||||
<label>
|
||||
<span>Name</span>
|
||||
<input id="workspace-name" type="text" value="${escapeHtml(draft.name)}" placeholder="Workspace name" />
|
||||
</label>
|
||||
<label>
|
||||
<span>Selected folder</span>
|
||||
<input id="workspace-path" type="text" value="${escapeHtml(draft.path)}" readonly placeholder="Select a folder from search results" />
|
||||
</label>
|
||||
<button id="save-workspace" class="primary-button" type="button" ${draft.path.trim() ? "" : "disabled"}>Add workspace</button>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderActiveDialog(): string {
|
||||
const state = appStore.getState();
|
||||
|
||||
if (!state.activeDialog) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const title = state.activeDialog === "add-url" ? "Add URL" : "Add workspace";
|
||||
const body = state.activeDialog === "add-url" ? renderAddUrl() : renderWorkspaceDialog();
|
||||
|
||||
return `
|
||||
<div class="app-modal" role="dialog" aria-modal="true" aria-label="${escapeHtml(title)}">
|
||||
<article class="app-modal-panel">
|
||||
<div class="app-modal-heading">
|
||||
<h2>${escapeHtml(title)}</h2>
|
||||
<button class="secondary-button compact-button icon-only-button" type="button" data-close-app-dialog title="Close" aria-label="Close">
|
||||
<i data-lucide="x"></i>
|
||||
</button>
|
||||
</div>
|
||||
${body}
|
||||
</article>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderFileList(files: File[], target: "issue" | "comment" | "issue-attachment"): string {
|
||||
if (!files.length) {
|
||||
return `<span class="muted">No files selected.</span>`;
|
||||
@@ -1025,7 +1115,35 @@ function openUrl(url: string): void {
|
||||
window.open(url, "_blank", "noopener,noreferrer");
|
||||
}
|
||||
|
||||
function bindWorkspaceFolderResultActions(): void {
|
||||
viewContent?.querySelectorAll<HTMLButtonElement>("[data-select-workspace-folder]").forEach((button) => {
|
||||
button.addEventListener("click", () => {
|
||||
const path = button.dataset.selectWorkspaceFolder;
|
||||
const name = button.dataset.folderName;
|
||||
|
||||
if (path) {
|
||||
appStore.getState().setWorkspaceDraft({
|
||||
path,
|
||||
name: appStore.getState().workspaceDraft.name || name || ""
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function bindViewActions(): void {
|
||||
viewContent?.querySelector<HTMLButtonElement>("#open-add-url-dialog")?.addEventListener("click", () => {
|
||||
appStore.getState().openDialog("add-url");
|
||||
});
|
||||
|
||||
viewContent?.querySelector<HTMLButtonElement>("#open-workspace-dialog")?.addEventListener("click", () => {
|
||||
appStore.getState().openDialog("add-workspace");
|
||||
});
|
||||
|
||||
viewContent?.querySelector<HTMLButtonElement>("[data-close-app-dialog]")?.addEventListener("click", () => {
|
||||
appStore.getState().closeDialog();
|
||||
});
|
||||
|
||||
viewContent?.querySelector<HTMLInputElement>("#agent-name")?.addEventListener("input", (event) => {
|
||||
appStore.getState().setAgentDraft({
|
||||
name: (event.target as HTMLInputElement).value
|
||||
@@ -1082,12 +1200,14 @@ function bindViewActions(): void {
|
||||
});
|
||||
});
|
||||
|
||||
viewContent?.querySelector<HTMLInputElement>("#workspace-path")?.addEventListener("input", (event) => {
|
||||
viewContent?.querySelector<HTMLInputElement>("#workspace-search")?.addEventListener("input", (event) => {
|
||||
appStore.getState().setWorkspaceDraft({
|
||||
path: (event.target as HTMLInputElement).value
|
||||
search: (event.target as HTMLInputElement).value
|
||||
});
|
||||
});
|
||||
|
||||
bindWorkspaceFolderResultActions();
|
||||
|
||||
viewContent?.querySelector<HTMLButtonElement>("#save-workspace")?.addEventListener("click", () => {
|
||||
void appStore.getState().saveWorkspace();
|
||||
});
|
||||
@@ -1566,6 +1686,7 @@ function renderView(): void {
|
||||
viewContent.innerHTML = renderUrls(allUrls());
|
||||
}
|
||||
|
||||
viewContent.insertAdjacentHTML("beforeend", renderActiveDialog());
|
||||
viewContent.insertAdjacentHTML("beforeend", renderScreenshotPreview());
|
||||
hydrateIcons();
|
||||
bindViewActions();
|
||||
@@ -1583,14 +1704,14 @@ function renderChrome(): void {
|
||||
refreshData.disabled = state.isLoadingData;
|
||||
}
|
||||
|
||||
if (addUrlButton) {
|
||||
addUrlButton.disabled = !state.activeBaseUrl || isActiveBaseUrlKnown();
|
||||
}
|
||||
|
||||
if (addIssueButton) {
|
||||
addIssueButton.disabled = !canAddIssue();
|
||||
}
|
||||
|
||||
if (manageViewSelect) {
|
||||
manageViewSelect.value = ["agents", "prompts", "workspaces"].includes(state.activeView) ? state.activeView : "";
|
||||
}
|
||||
|
||||
viewButtons.forEach((button) => {
|
||||
const view = button.dataset.view;
|
||||
button.dataset.active = view === state.activeView ? "true" : "false";
|
||||
@@ -1605,7 +1726,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, #prompt-name, #prompt-content, #workspace-name, #workspace-path, [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, #workspace-name, #workspace-search, [data-label-target]"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1615,6 +1736,9 @@ function syncFocusedFormControls(): void {
|
||||
const saveSelectedIssue = viewContent?.querySelector<HTMLButtonElement>("#save-selected-issue");
|
||||
const submitComment = viewContent?.querySelector<HTMLButtonElement>("#submit-issue-comment");
|
||||
const saveWorkspace = viewContent?.querySelector<HTMLButtonElement>("#save-workspace");
|
||||
const workspaceResults = viewContent?.querySelector<HTMLElement>("#workspace-folder-results");
|
||||
const workspacePath = viewContent?.querySelector<HTMLInputElement>("#workspace-path");
|
||||
const workspaceName = viewContent?.querySelector<HTMLInputElement>("#workspace-name");
|
||||
|
||||
if (createIssue) {
|
||||
createIssue.disabled = !state.issueDraft.subject.trim() || state.isCreatingIssue;
|
||||
@@ -1632,6 +1756,19 @@ function syncFocusedFormControls(): void {
|
||||
saveWorkspace.disabled = !state.workspaceDraft.path.trim();
|
||||
}
|
||||
|
||||
if (workspaceResults) {
|
||||
workspaceResults.innerHTML = renderWorkspaceSearchResults(workspaceFolderCandidates(), state.workspaceDraft.search);
|
||||
bindWorkspaceFolderResultActions();
|
||||
}
|
||||
|
||||
if (workspacePath) {
|
||||
workspacePath.value = state.workspaceDraft.path;
|
||||
}
|
||||
|
||||
if (workspaceName && document.activeElement !== workspaceName) {
|
||||
workspaceName.value = state.workspaceDraft.name;
|
||||
}
|
||||
|
||||
if (saveSelectedIssue && state.selectedIssueDetail) {
|
||||
const detailLabels = ISSUE_LABEL_OPTIONS.filter((label) =>
|
||||
state.selectedIssueDetail?.issue.labels.some((issueLabel) => issueLabel.toLowerCase() === label.toLowerCase())
|
||||
@@ -1693,6 +1830,13 @@ viewButtons.forEach((button) => {
|
||||
});
|
||||
});
|
||||
|
||||
manageViewSelect?.addEventListener("change", () => {
|
||||
const view = manageViewSelect.value as AppView;
|
||||
if (view === "agents" || view === "prompts" || view === "workspaces") {
|
||||
appStore.getState().setActiveView(view);
|
||||
}
|
||||
});
|
||||
|
||||
refreshData?.addEventListener("click", () => {
|
||||
void appStore.getState().loadData();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user