feat: add workspace-aware extension dashboard
Build / build (push) Successful in 12s

This commit is contained in:
2026-07-17 15:02:08 -05:00
parent 8351fc65f5
commit 85379f42f3
14 changed files with 1369 additions and 94 deletions
+286 -20
View File
@@ -1,29 +1,222 @@
import { createIcons, CirclePlus, FolderGit2, GitBranch, Link, List, RefreshCw } from "lucide";
import "./styles.css";
import { appStore } from "./store";
import { appStore, type AppView } from "./store";
import type { GiteaRepoSummary } from "./api";
import type { KnownUrl, LocalWorkspace } from "./localContext";
const appWindow = document.querySelector<HTMLElement>("#app-window");
const dragHandle = document.querySelector<HTMLElement>("#window-drag-handle");
const configBadge = document.querySelector<HTMLSpanElement>("#config-badge");
const baseUrl = document.querySelector<HTMLElement>("#base-url");
const tokenState = document.querySelector<HTMLElement>("#token-state");
const targetRepo = document.querySelector<HTMLElement>("#target-repo");
const status = document.querySelector<HTMLElement>("#status");
const testConnection = document.querySelector<HTMLButtonElement>("#test-connection");
const refreshData = document.querySelector<HTMLButtonElement>("#refresh-data");
const viewHeader = document.querySelector<HTMLElement>("#view-header");
const viewContent = document.querySelector<HTMLElement>("#view-content");
const viewButtons = Array.from(document.querySelectorAll<HTMLButtonElement>("[data-view]"));
function setText(element: HTMLElement | null, text: string): void {
if (element) {
element.textContent = text;
createIcons({
icons: {
CirclePlus,
FolderGit2,
GitBranch,
Link,
List,
RefreshCw
}
});
function escapeHtml(value: string): string {
return value.replace(/[&<>"']/g, (char) => {
const replacements: Record<string, string> = {
"&": "&amp;",
"<": "&lt;",
">": "&gt;",
"\"": "&quot;",
"'": "&#39;"
};
return replacements[char];
});
}
function render(): void {
function formatDate(value: string): string {
if (!value) {
return "No commit date";
}
return new Intl.DateTimeFormat(undefined, {
dateStyle: "medium",
timeStyle: "short"
}).format(new Date(value));
}
function formatSha(value: string): string {
return value ? value.slice(0, 8) : "unknown";
}
function getRepoWorkspaceLink(repo: GiteaRepoSummary): string {
const context = appStore.getState().localContext;
const worktree = context?.gitWorktrees.find((item) => item.giteaRepo?.fullName === repo.fullName);
if (!worktree) {
return `<span class="muted">No workspace link</span>`;
}
return `<span class="link-chip">Workspace link</span><span class="path-text">${escapeHtml(worktree.path)}</span>`;
}
function renderRepos(repos: GiteaRepoSummary[]): string {
if (!repos.length) {
return `<div class="empty-state">No Gitea repos loaded yet.</div>`;
}
return repos
.map(
(repo) => `
<article class="item-card">
<div class="item-heading">
<div>
<a class="item-title" href="${repo.webUrl}" target="_blank" rel="noreferrer">${escapeHtml(repo.fullName)}</a>
<p class="item-subtitle">${escapeHtml(repo.description || "No description")}</p>
</div>
<span class="badge quiet">${repo.isPrivate ? "Private" : "Public"}</span>
</div>
<div class="meta-grid">
<span>Branch: <strong>${escapeHtml(repo.defaultBranch)}</strong></span>
<span>Last commit: <strong>${formatDate(repo.lastCommitAt)}</strong></span>
<span>SHA: <strong>${formatSha(repo.lastCommitSha)}</strong></span>
</div>
<p class="commit-message">${escapeHtml(repo.lastCommitMessage)}</p>
<div class="linked-row">${getRepoWorkspaceLink(repo)}</div>
</article>
`
)
.join("");
}
function renderWorkspaces(workspaces: LocalWorkspace[]): string {
if (!workspaces.length) {
return `<div class="empty-state">No workspaces found in workspace_Father.</div>`;
}
return 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="item-card">
<div class="item-heading">
<div>
<h2 class="item-title">${escapeHtml(workspace.name)}</h2>
<p class="path-text">${escapeHtml(workspace.path)}</p>
</div>
${workspace.linkedRepos.length ? `<span class="link-chip">Linked</span>` : ""}
</div>
<div class="repo-link-list">${links}</div>
</article>
`;
})
.join("");
}
function renderUrls(urls: KnownUrl[]): string {
if (!urls.length) {
return `<div class="empty-state">No known URLs registered.</div>`;
}
return urls
.map(
(knownUrl) => `
<article class="item-card">
<div class="item-heading">
<div>
<h2 class="item-title">${escapeHtml(knownUrl.label)}</h2>
<a class="url-text" href="${knownUrl.url}" target="_blank" rel="noreferrer">${escapeHtml(knownUrl.url)}</a>
</div>
<button class="visit-button" type="button" data-open-url="${knownUrl.url}">Quick visit</button>
</div>
<div class="linked-row">
<span class="link-chip">Workspace link</span>
<span class="path-text">${escapeHtml(knownUrl.workspacePath)}</span>
</div>
<div class="linked-row">
<span class="link-chip">Gitea link</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>
</article>
`
)
.join("");
}
function openUrl(url: string): void {
if (globalThis.chrome?.tabs?.create) {
void globalThis.chrome.tabs.create({ url });
return;
}
window.open(url, "_blank", "noopener,noreferrer");
}
function renderView(): void {
if (!viewHeader || !viewContent) {
return;
}
const state = appStore.getState();
const context = state.localContext;
const titles: Record<AppView, string> = {
urls: "URLs",
repos: "Gitea repos",
workspaces: "Workspaces"
};
const descriptions: Record<AppView, string> = {
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"}.`
};
viewHeader.innerHTML = `
<div>
<h2>${titles[state.activeView]}</h2>
<p>${escapeHtml(descriptions[state.activeView])}</p>
</div>
`;
if (state.activeView === "repos") {
viewContent.innerHTML = renderRepos(state.repos);
} else if (state.activeView === "workspaces") {
viewContent.innerHTML = renderWorkspaces(context?.workspaces ?? []);
} else {
viewContent.innerHTML = renderUrls(context?.knownUrls ?? []);
}
viewContent.querySelectorAll<HTMLButtonElement>("[data-open-url]").forEach((button) => {
button.addEventListener("click", () => {
const url = button.dataset.openUrl;
if (url) {
openUrl(url);
}
});
});
}
function renderChrome(): void {
const state = appStore.getState();
const config = state.config;
setText(baseUrl, config.baseUrl || "Not configured");
setText(tokenState, config.hasToken ? "Configured" : "Not configured");
setText(targetRepo, config.targetRepo || "Not configured");
setText(configBadge, config.isConfigured ? "Configured" : "Missing env");
if (configBadge) {
configBadge.textContent = config.isConfigured ? "Configured" : "Missing env";
configBadge.dataset.tone = config.isConfigured ? "success" : "warning";
}
@@ -32,14 +225,87 @@ function render(): void {
status.dataset.tone = state.statusTone;
}
if (testConnection) {
testConnection.disabled = state.isTestingConnection;
if (refreshData) {
refreshData.disabled = state.isLoadingData;
}
if (appWindow) {
appWindow.style.transform = `translate(${state.windowPosition.x}px, ${state.windowPosition.y}px)`;
}
viewButtons.forEach((button) => {
const view = button.dataset.view;
button.dataset.active = view === state.activeView ? "true" : "false";
});
}
function render(): void {
renderChrome();
renderView();
}
function setupDragging(): void {
if (!appWindow || !dragHandle) {
return;
}
let dragStart:
| {
pointerId: number;
pointerX: number;
pointerY: number;
windowX: number;
windowY: number;
}
| null = null;
dragHandle.addEventListener("pointerdown", (event) => {
const state = appStore.getState();
dragStart = {
pointerId: event.pointerId,
pointerX: event.clientX,
pointerY: event.clientY,
windowX: state.windowPosition.x,
windowY: state.windowPosition.y
};
dragHandle.setPointerCapture(event.pointerId);
});
dragHandle.addEventListener("pointermove", (event) => {
if (!dragStart || event.pointerId !== dragStart.pointerId) {
return;
}
const rect = appWindow.getBoundingClientRect();
const maxX = Math.max(0, window.innerWidth - rect.width);
const maxY = Math.max(0, window.innerHeight - rect.height);
const x = Math.min(Math.max(0, dragStart.windowX + event.clientX - dragStart.pointerX), maxX);
const y = Math.min(Math.max(0, dragStart.windowY + event.clientY - dragStart.pointerY), maxY);
appStore.getState().setWindowPosition({ x, y });
});
dragHandle.addEventListener("pointerup", (event) => {
if (dragStart?.pointerId === event.pointerId) {
dragStart = null;
dragHandle.releasePointerCapture(event.pointerId);
}
});
}
viewButtons.forEach((button) => {
button.addEventListener("click", () => {
const view = button.dataset.view as AppView | undefined;
if (view) {
appStore.getState().setActiveView(view);
}
});
});
refreshData?.addEventListener("click", () => {
void appStore.getState().loadData();
});
setupDragging();
appStore.subscribe(render);
render();
testConnection?.addEventListener("click", () => {
void appStore.getState().testConnection();
});
void appStore.getState().loadData();