Consolidate management navigation
Build / build (push) Successful in 11s

This commit is contained in:
2026-07-21 07:42:30 -05:00
parent b4045eefc9
commit c663f523ad
12 changed files with 8286 additions and 147 deletions
+237 -93
View File
@@ -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();
});