diff --git a/AGENTS.md b/AGENTS.md
index 146ecd8..b669126 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -37,10 +37,13 @@ Use `.env.local` for local secrets. Keep `.env.local` and any real token-bearing
- `dist/` is the deployable output directory. Deployment or Chrome load-unpacked flows should point directly at `dist/`.
- `dist/` is generated output and should not be edited directly.
- Static extension assets live in `public/`; the current favicon source is `public/favicon.svg`.
+- `scripts/generate-local-context.mjs` generates `public/local-context.json` from `/Users/Tabitha/.openclaw/workspace_Father` before builds.
+- `public/local-context.json` is a committed fallback snapshot so CI builds can still produce a self-contained `dist/` if the local workspace root is unavailable.
- `npm run build` runs `scripts/verify-dist.mjs` after Vite to ensure the generated output has the required extension files.
- Host permissions are derived from `VITE_GITEA_BASE_URL`. Rebuild after changing the base URL.
- Runtime API calls are centralized in `src/api.ts`.
- Popup state is centralized in the vanilla Zustand store at `src/store.ts`.
+- The popup UI is a movable drag/drop window with icon menu sections for URLs, Gitea repos, and workspaces. Add URL and Add Issue are intentionally disabled.
## Chrome Extension Management
diff --git a/README.md b/README.md
index 7a54233..49c7196 100644
--- a/README.md
+++ b/README.md
@@ -26,6 +26,10 @@ The deployable extension output is `dist/`. Load `dist/` in Chrome as an unpacke
Static assets, including the favicon, live in `public/` and are copied into `dist/` during the build.
+The app opens as a draggable window inside the extension popup. The top icon menu includes URLs, Gitea repos, and workspaces; Add URL and Add Issue are present but disabled for now.
+
+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.
+
## Extension Management
See [docs/extension-management.md](docs/extension-management.md) before loading, reloading, testing, or automating the Chrome extension.
diff --git a/index.html b/index.html
index 4978324..6221a1c 100644
--- a/index.html
+++ b/index.html
@@ -7,33 +7,50 @@
Silma AI Aide
-
-
+
+
+
-
-
-
-
Base URL
- Not configured
-
-
-
Token
- Not configured
-
-
-
Target repo
- Not configured
-
-
+
+
+
+ Add URL
+
+
+
+ URLs
+
+
+
+ Gitea Repos
+
+
+
+ Workspaces
+
+
+
+ Add Issue
+
+
- Test connection
- Ready.
+
+
+
diff --git a/llm.txt b/llm.txt
index 37fc1bd..77f48b4 100644
--- a/llm.txt
+++ b/llm.txt
@@ -14,9 +14,13 @@ Important files:
- `src/main.ts`: Popup UI behavior.
- `index.html`: Popup document.
- `public/favicon.svg`: Source favicon copied into `dist/` by Vite.
+- `public/local-context.json`: Generated workspace/repo/URL context copied into `dist/`.
+- `scripts/generate-local-context.mjs`: Scans `/Users/Tabitha/.openclaw/workspace_Father` and writes `public/local-context.json`.
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 movable drag/drop window inside the browser surface. The menu items are Add URL (disabled), URLs, Gitea Repos, Workspaces, and Add Issue (disabled). 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`.
Codex commands:
diff --git a/manifest.llm.json b/manifest.llm.json
index acb490e..b273257 100644
--- a/manifest.llm.json
+++ b/manifest.llm.json
@@ -15,10 +15,46 @@
"popup_script": "src/main.ts",
"state_store": "src/store.ts",
"api_client": "src/api.ts",
+ "local_context_types": "src/localContext.ts",
"vite_config": "vite.config.ts",
"favicon": "public/favicon.svg",
+ "local_context": "public/local-context.json",
+ "local_context_generator": "scripts/generate-local-context.mjs",
"generated_manifest": "dist/manifest.json"
},
+ "ui": {
+ "shell": "movable drag/drop window inside the browser surface",
+ "menu_items": [
+ {
+ "name": "Add URL",
+ "enabled": false
+ },
+ {
+ "name": "URLs",
+ "enabled": true
+ },
+ {
+ "name": "Gitea Repos",
+ "enabled": true
+ },
+ {
+ "name": "Workspaces",
+ "enabled": true
+ },
+ {
+ "name": "Add Issue",
+ "enabled": false
+ }
+ ],
+ "repo_ordering": "Gitea repos are ordered by latest default-branch commit timestamp.",
+ "known_urls": [
+ {
+ "url": "http://100.66.226.22:23030/",
+ "workspace": "/Users/Tabitha/.openclaw/workspace_Father/workspace_Data",
+ "gitea_repo": "jacob-mathison/home-grown-llm-data"
+ }
+ ]
+ },
"documentation": {
"agent_instructions": "AGENTS.md",
"llm_summary": "llm.txt",
diff --git a/package-lock.json b/package-lock.json
index da89515..03cd5a2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -11,6 +11,7 @@
"dependencies": {
"axios": "^1.18.1",
"dotenv": "^17.4.2",
+ "lucide": "^1.25.0",
"zustand": "^5.0.14"
},
"devDependencies": {
@@ -1375,6 +1376,12 @@
"url": "https://opencollective.com/parcel"
}
},
+ "node_modules/lucide": {
+ "version": "1.25.0",
+ "resolved": "https://registry.npmjs.org/lucide/-/lucide-1.25.0.tgz",
+ "integrity": "sha512-Pg/9Ga1xTbrnI6GY/7J9krIYmJIkAf3PkK6wPmsAeUtAQsBdGxs1+dfvLqjf9nPI5aFm9+VquT75JGpO3QXSpQ==",
+ "license": "ISC"
+ },
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
diff --git a/package.json b/package.json
index a414469..3f10185 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,7 @@
"codex:setup": "npm ci",
"dev": "vite --host 0.0.0.0",
"dev:codex": "npm run dev",
+ "prebuild": "node scripts/generate-local-context.mjs",
"build": "tsc --noEmit && vite build && npm run verify:dist",
"build:codex": "npm run build",
"preview": "vite preview --host 0.0.0.0",
@@ -22,6 +23,7 @@
"dependencies": {
"axios": "^1.18.1",
"dotenv": "^17.4.2",
+ "lucide": "^1.25.0",
"zustand": "^5.0.14"
},
"devDependencies": {
diff --git a/public/local-context.json b/public/local-context.json
new file mode 100644
index 0000000..b43c896
--- /dev/null
+++ b/public/local-context.json
@@ -0,0 +1,306 @@
+{
+ "generatedAt": "build-time-local-context",
+ "workspaceRoot": "/Users/Tabitha/.openclaw/workspace_Father",
+ "workspaces": [
+ {
+ "name": "aarete",
+ "path": "/Users/Tabitha/.openclaw/workspace_Father/aarete",
+ "linkedRepos": [
+ {
+ "worktreePath": "/Users/Tabitha/.openclaw/workspace_Father/aarete/workspace/aarete-doczyai-app",
+ "worktreeRelativePath": "aarete/workspace/aarete-doczyai-app",
+ "fullName": "jacob-mathison/aarete-doczyai-app",
+ "webUrl": "http://gitea.orson.tealthrone/jacob-mathison/aarete-doczyai-app"
+ },
+ {
+ "worktreePath": "/Users/Tabitha/.openclaw/workspace_Father/aarete/workspace/aarete-query-orchestration",
+ "worktreeRelativePath": "aarete/workspace/aarete-query-orchestration",
+ "fullName": "jacob-mathison/query-orchestration",
+ "webUrl": "http://gitea.orson.tealthrone/jacob-mathison/query-orchestration"
+ },
+ {
+ "worktreePath": "/Users/Tabitha/.openclaw/workspace_Father/aarete/workspace/doczyai-pipelines",
+ "worktreeRelativePath": "aarete/workspace/doczyai-pipelines",
+ "fullName": "jacob-mathison/doczyai-pipelines",
+ "webUrl": "http://gitea.orson.tealthrone/jacob-mathison/doczyai-pipelines"
+ }
+ ]
+ },
+ {
+ "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",
+ "linkedRepos": [
+ {
+ "worktreePath": "/Users/Tabitha/.openclaw/workspace_Father/banner-and-pyre/mp-banner-and-pyre",
+ "worktreeRelativePath": "banner-and-pyre/mp-banner-and-pyre",
+ "fullName": "jacob-mathison/mp-banner-and-pyre",
+ "webUrl": "http://gitea.orson.tealthrone/jacob-mathison/mp-banner-and-pyre"
+ },
+ {
+ "worktreePath": "/Users/Tabitha/.openclaw/workspace_Father/banner-and-pyre/mp-banner-and-pyre-migration",
+ "worktreeRelativePath": "banner-and-pyre/mp-banner-and-pyre-migration",
+ "fullName": "jacob-mathison/mp-banner-and-pyre-migration",
+ "webUrl": "http://gitea.orson.tealthrone/jacob-mathison/mp-banner-and-pyre-migration"
+ },
+ {
+ "worktreePath": "/Users/Tabitha/.openclaw/workspace_Father/banner-and-pyre/mp-banner-and-pyre-web",
+ "worktreeRelativePath": "banner-and-pyre/mp-banner-and-pyre-web",
+ "fullName": "jacob-mathison/mp-banner-and-pyre-web",
+ "webUrl": "http://gitea.orson.tealthrone/jacob-mathison/mp-banner-and-pyre-web"
+ }
+ ]
+ },
+ {
+ "name": "blb-throne-of-the-bone-king",
+ "path": "/Users/Tabitha/.openclaw/workspace_Father/blb-throne-of-the-bone-king",
+ "linkedRepos": [
+ {
+ "worktreePath": "/Users/Tabitha/.openclaw/workspace_Father/blb-throne-of-the-bone-king",
+ "worktreeRelativePath": "blb-throne-of-the-bone-king",
+ "fullName": "jacob-mathison/blb-throne-of-the-bone-king",
+ "webUrl": "http://gitea.orson.tealthrone/jacob-mathison/blb-throne-of-the-bone-king"
+ }
+ ]
+ },
+ {
+ "name": "midnights-eclipse-migration",
+ "path": "/Users/Tabitha/.openclaw/workspace_Father/midnights-eclipse-migration",
+ "linkedRepos": [
+ {
+ "worktreePath": "/Users/Tabitha/.openclaw/workspace_Father/midnights-eclipse-migration",
+ "worktreeRelativePath": "midnights-eclipse-migration",
+ "fullName": "jacob-mathison/midnights-eclipse-migration",
+ "webUrl": "http://gitea.orson.tealthrone/jacob-mathison/midnights-eclipse-migration"
+ }
+ ]
+ },
+ {
+ "name": "mp-silma-ai-aide",
+ "path": "/Users/Tabitha/.openclaw/workspace_Father/mp-silma-ai-aide",
+ "linkedRepos": [
+ {
+ "worktreePath": "/Users/Tabitha/.openclaw/workspace_Father/mp-silma-ai-aide",
+ "worktreeRelativePath": "mp-silma-ai-aide",
+ "fullName": "jacob-mathison/mp-silma-ai-aide",
+ "webUrl": "http://gitea.orson.tealthrone/jacob-mathison/mp-silma-ai-aide"
+ }
+ ]
+ },
+ {
+ "name": "tuono",
+ "path": "/Users/Tabitha/.openclaw/workspace_Father/tuono",
+ "linkedRepos": []
+ },
+ {
+ "name": "workspace_Data",
+ "path": "/Users/Tabitha/.openclaw/workspace_Father/workspace_Data",
+ "linkedRepos": [
+ {
+ "worktreePath": "/Users/Tabitha/.openclaw/workspace_Father/workspace_Data",
+ "worktreeRelativePath": "workspace_Data",
+ "fullName": "jacob-mathison/home-grown-llm-data",
+ "webUrl": "http://gitea.orson.tealthrone/jacob-mathison/home-grown-llm-data"
+ }
+ ]
+ }
+ ],
+ "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",
+ "relativePath": "aarete/automations",
+ "topWorkspaceName": "aarete",
+ "branch": "main",
+ "giteaRepo": null
+ },
+ {
+ "name": "aarete-doczyai-app",
+ "path": "/Users/Tabitha/.openclaw/workspace_Father/aarete/workspace/aarete-doczyai-app",
+ "relativePath": "aarete/workspace/aarete-doczyai-app",
+ "topWorkspaceName": "aarete",
+ "branch": "main",
+ "giteaRepo": {
+ "fullName": "jacob-mathison/aarete-doczyai-app",
+ "owner": "jacob-mathison",
+ "name": "aarete-doczyai-app",
+ "webUrl": "http://gitea.orson.tealthrone/jacob-mathison/aarete-doczyai-app",
+ "cloneUrl": "http://100.79.253.19:3000/jacob-mathison/aarete-doczyai-app.git"
+ }
+ },
+ {
+ "name": "aarete-query-orchestration",
+ "path": "/Users/Tabitha/.openclaw/workspace_Father/aarete/workspace/aarete-query-orchestration",
+ "relativePath": "aarete/workspace/aarete-query-orchestration",
+ "topWorkspaceName": "aarete",
+ "branch": "main",
+ "giteaRepo": {
+ "fullName": "jacob-mathison/query-orchestration",
+ "owner": "jacob-mathison",
+ "name": "query-orchestration",
+ "webUrl": "http://gitea.orson.tealthrone/jacob-mathison/query-orchestration",
+ "cloneUrl": "http://100.79.253.19:3000/jacob-mathison/query-orchestration.git"
+ }
+ },
+ {
+ "name": "doczyai-pipelines",
+ "path": "/Users/Tabitha/.openclaw/workspace_Father/aarete/workspace/doczyai-pipelines",
+ "relativePath": "aarete/workspace/doczyai-pipelines",
+ "topWorkspaceName": "aarete",
+ "branch": "main",
+ "giteaRepo": {
+ "fullName": "jacob-mathison/doczyai-pipelines",
+ "owner": "jacob-mathison",
+ "name": "doczyai-pipelines",
+ "webUrl": "http://gitea.orson.tealthrone/jacob-mathison/doczyai-pipelines",
+ "cloneUrl": "http://100.79.253.19:3000/jacob-mathison/doczyai-pipelines.git"
+ }
+ },
+ {
+ "name": "mp-banner-and-pyre",
+ "path": "/Users/Tabitha/.openclaw/workspace_Father/banner-and-pyre/mp-banner-and-pyre",
+ "relativePath": "banner-and-pyre/mp-banner-and-pyre",
+ "topWorkspaceName": "banner-and-pyre",
+ "branch": "main",
+ "giteaRepo": {
+ "fullName": "jacob-mathison/mp-banner-and-pyre",
+ "owner": "jacob-mathison",
+ "name": "mp-banner-and-pyre",
+ "webUrl": "http://gitea.orson.tealthrone/jacob-mathison/mp-banner-and-pyre",
+ "cloneUrl": "http://100.79.253.19:3000/jacob-mathison/mp-banner-and-pyre.git"
+ }
+ },
+ {
+ "name": "mp-banner-and-pyre-migration",
+ "path": "/Users/Tabitha/.openclaw/workspace_Father/banner-and-pyre/mp-banner-and-pyre-migration",
+ "relativePath": "banner-and-pyre/mp-banner-and-pyre-migration",
+ "topWorkspaceName": "banner-and-pyre",
+ "branch": "master",
+ "giteaRepo": {
+ "fullName": "jacob-mathison/mp-banner-and-pyre-migration",
+ "owner": "jacob-mathison",
+ "name": "mp-banner-and-pyre-migration",
+ "webUrl": "http://gitea.orson.tealthrone/jacob-mathison/mp-banner-and-pyre-migration",
+ "cloneUrl": "http://100.79.253.19:3000/jacob-mathison/mp-banner-and-pyre-migration.git"
+ }
+ },
+ {
+ "name": "mp-banner-and-pyre-web",
+ "path": "/Users/Tabitha/.openclaw/workspace_Father/banner-and-pyre/mp-banner-and-pyre-web",
+ "relativePath": "banner-and-pyre/mp-banner-and-pyre-web",
+ "topWorkspaceName": "banner-and-pyre",
+ "branch": "main",
+ "giteaRepo": {
+ "fullName": "jacob-mathison/mp-banner-and-pyre-web",
+ "owner": "jacob-mathison",
+ "name": "mp-banner-and-pyre-web",
+ "webUrl": "http://gitea.orson.tealthrone/jacob-mathison/mp-banner-and-pyre-web",
+ "cloneUrl": "http://100.79.253.19:3000/jacob-mathison/mp-banner-and-pyre-web.git"
+ }
+ },
+ {
+ "name": "blb-throne-of-the-bone-king",
+ "path": "/Users/Tabitha/.openclaw/workspace_Father/blb-throne-of-the-bone-king",
+ "relativePath": "blb-throne-of-the-bone-king",
+ "topWorkspaceName": "blb-throne-of-the-bone-king",
+ "branch": "main",
+ "giteaRepo": {
+ "fullName": "jacob-mathison/blb-throne-of-the-bone-king",
+ "owner": "jacob-mathison",
+ "name": "blb-throne-of-the-bone-king",
+ "webUrl": "http://gitea.orson.tealthrone/jacob-mathison/blb-throne-of-the-bone-king",
+ "cloneUrl": "http://100.79.253.19:3000/jacob-mathison/blb-throne-of-the-bone-king.git"
+ }
+ },
+ {
+ "name": "midnights-eclipse-migration",
+ "path": "/Users/Tabitha/.openclaw/workspace_Father/midnights-eclipse-migration",
+ "relativePath": "midnights-eclipse-migration",
+ "topWorkspaceName": "midnights-eclipse-migration",
+ "branch": "main",
+ "giteaRepo": {
+ "fullName": "jacob-mathison/midnights-eclipse-migration",
+ "owner": "jacob-mathison",
+ "name": "midnights-eclipse-migration",
+ "webUrl": "http://gitea.orson.tealthrone/jacob-mathison/midnights-eclipse-migration",
+ "cloneUrl": "http://100.79.253.19:3000/jacob-mathison/midnights-eclipse-migration.git"
+ }
+ },
+ {
+ "name": "mp-silma-ai-aide",
+ "path": "/Users/Tabitha/.openclaw/workspace_Father/mp-silma-ai-aide",
+ "relativePath": "mp-silma-ai-aide",
+ "topWorkspaceName": "mp-silma-ai-aide",
+ "branch": "main",
+ "giteaRepo": {
+ "fullName": "jacob-mathison/mp-silma-ai-aide",
+ "owner": "jacob-mathison",
+ "name": "mp-silma-ai-aide",
+ "webUrl": "http://gitea.orson.tealthrone/jacob-mathison/mp-silma-ai-aide",
+ "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",
+ "relativePath": "workspace_Data",
+ "topWorkspaceName": "workspace_Data",
+ "branch": "main",
+ "giteaRepo": {
+ "fullName": "jacob-mathison/home-grown-llm-data",
+ "owner": "jacob-mathison",
+ "name": "home-grown-llm-data",
+ "webUrl": "http://gitea.orson.tealthrone/jacob-mathison/home-grown-llm-data",
+ "cloneUrl": "http://100.79.253.19:3000/jacob-mathison/home-grown-llm-data.git"
+ }
+ }
+ ],
+ "knownUrls": [
+ {
+ "label": "Automated Training Monitor",
+ "url": "http://100.66.226.22:23030/",
+ "workspacePath": "/Users/Tabitha/.openclaw/workspace_Father/workspace_Data",
+ "workspaceName": "workspace_Data",
+ "giteaRepo": {
+ "fullName": "jacob-mathison/home-grown-llm-data",
+ "owner": "jacob-mathison",
+ "name": "home-grown-llm-data",
+ "webUrl": "http://gitea.orson.tealthrone/jacob-mathison/home-grown-llm-data",
+ "cloneUrl": "http://100.79.253.19:3000/jacob-mathison/home-grown-llm-data.git"
+ }
+ }
+ ]
+}
diff --git a/scripts/generate-local-context.mjs b/scripts/generate-local-context.mjs
new file mode 100644
index 0000000..f1770fa
--- /dev/null
+++ b/scripts/generate-local-context.mjs
@@ -0,0 +1,158 @@
+import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
+import { basename, join, relative } from "node:path";
+import { execFileSync } from "node:child_process";
+
+const workspaceRoot =
+ process.env.WORKSPACE_FATHER_PATH || "/Users/Tabitha/.openclaw/workspace_Father";
+const outputPath = join(process.cwd(), "public", "local-context.json");
+const giteaBaseUrl = "http://gitea.orson.tealthrone";
+
+function runGit(args, cwd) {
+ try {
+ return execFileSync("git", args, { cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
+ } catch {
+ return "";
+ }
+}
+
+function findGitWorktrees(root, maxDepth = 4) {
+ const found = [];
+
+ function walk(dir, depth) {
+ if (depth > maxDepth) {
+ return;
+ }
+
+ if (existsSync(join(dir, ".git"))) {
+ found.push(dir);
+ return;
+ }
+
+ let entries = [];
+ try {
+ entries = readdirSync(dir, { withFileTypes: true });
+ } catch {
+ return;
+ }
+
+ for (const entry of entries) {
+ if (!entry.isDirectory() || entry.name === "node_modules" || entry.name === ".git") {
+ continue;
+ }
+ walk(join(dir, entry.name), depth + 1);
+ }
+ }
+
+ walk(root, 0);
+ return found.sort();
+}
+
+function normalizeGiteaRemote(remoteUrl) {
+ if (!remoteUrl) {
+ return null;
+ }
+
+ const match = remoteUrl.match(
+ /(?:gitea\.orson\.tealthrone|100\.79\.253\.19(?::3000)?)[/:]([^/]+)\/([^/.]+)(?:\.git)?$/
+ );
+ if (!match) {
+ return null;
+ }
+
+ const [, owner, name] = match;
+ return {
+ fullName: `${owner}/${name}`,
+ owner,
+ name,
+ webUrl: `${giteaBaseUrl}/${owner}/${name}`,
+ cloneUrl: `http://100.79.253.19:3000/${owner}/${name}.git`
+ };
+}
+
+function repoForWorktree(path) {
+ const remoteNames = runGit(["remote"], path).split(/\s+/).filter(Boolean);
+ const orderedNames = ["gitea", "hagia", "origin", ...remoteNames].filter((name, index, names) => {
+ return remoteNames.includes(name) && names.indexOf(name) === index;
+ });
+
+ for (const remoteName of orderedNames) {
+ const repo = normalizeGiteaRemote(runGit(["remote", "get-url", remoteName], path));
+ if (repo) {
+ return repo;
+ }
+ }
+
+ return null;
+}
+
+function buildContext() {
+ const topLevelWorkspaces = readdirSync(workspaceRoot, { withFileTypes: true })
+ .filter((entry) => entry.isDirectory())
+ .map((entry) => ({
+ name: entry.name,
+ path: join(workspaceRoot, entry.name)
+ }))
+ .sort((a, b) => a.name.localeCompare(b.name));
+
+ const gitWorktrees = findGitWorktrees(workspaceRoot).map((path) => {
+ const repo = repoForWorktree(path);
+ const topWorkspaceName = relative(workspaceRoot, path).split("/")[0] || basename(path);
+ return {
+ name: basename(path),
+ path,
+ relativePath: relative(workspaceRoot, path),
+ topWorkspaceName,
+ branch: runGit(["branch", "--show-current"], path) || null,
+ giteaRepo: repo
+ };
+ });
+
+ const workspaces = topLevelWorkspaces.map((workspace) => ({
+ ...workspace,
+ linkedRepos: gitWorktrees
+ .filter((worktree) => worktree.topWorkspaceName === workspace.name && worktree.giteaRepo)
+ .map((worktree) => ({
+ worktreePath: worktree.path,
+ worktreeRelativePath: worktree.relativePath,
+ fullName: worktree.giteaRepo.fullName,
+ webUrl: worktree.giteaRepo.webUrl
+ }))
+ }));
+
+ const workspaceData = gitWorktrees.find(
+ (worktree) => worktree.path === join(workspaceRoot, "workspace_Data")
+ );
+
+ return {
+ generatedAt: "build-time-local-context",
+ workspaceRoot,
+ workspaces,
+ gitWorktrees,
+ knownUrls: [
+ {
+ label: "Automated Training Monitor",
+ url: "http://100.66.226.22:23030/",
+ workspacePath: join(workspaceRoot, "workspace_Data"),
+ workspaceName: "workspace_Data",
+ giteaRepo: workspaceData?.giteaRepo ?? {
+ fullName: "jacob-mathison/home-grown-llm-data",
+ webUrl: `${giteaBaseUrl}/jacob-mathison/home-grown-llm-data`
+ }
+ }
+ ]
+ };
+}
+
+mkdirSync(join(process.cwd(), "public"), { recursive: true });
+
+if (!existsSync(workspaceRoot)) {
+ if (existsSync(outputPath)) {
+ console.log(`Workspace root missing; preserving ${outputPath}`);
+ process.exit(0);
+ }
+ throw new Error(`Workspace root does not exist: ${workspaceRoot}`);
+}
+
+const context = buildContext();
+writeFileSync(outputPath, `${JSON.stringify(context, null, 2)}\n`);
+console.log(`Wrote ${outputPath} with ${context.workspaces.length} workspaces`);
diff --git a/src/api.ts b/src/api.ts
index 98289f5..d295797 100644
--- a/src/api.ts
+++ b/src/api.ts
@@ -12,6 +12,46 @@ export interface GiteaUser {
email?: string;
}
+interface GiteaRepoOwner {
+ login: string;
+}
+
+interface GiteaRepoResponse {
+ name: string;
+ full_name?: string;
+ description?: string;
+ html_url?: string;
+ clone_url?: string;
+ ssh_url?: string;
+ updated_at?: string;
+ default_branch?: string;
+ private?: boolean;
+ owner?: GiteaRepoOwner;
+}
+
+interface GiteaBranchResponse {
+ name: string;
+ commit?: {
+ id?: string;
+ message?: string;
+ timestamp?: string;
+ url?: string;
+ };
+}
+
+export interface GiteaRepoSummary {
+ name: string;
+ fullName: string;
+ description: string;
+ webUrl: string;
+ cloneUrl: string;
+ defaultBranch: string;
+ isPrivate: boolean;
+ lastCommitAt: string;
+ lastCommitSha: string;
+ lastCommitMessage: string;
+}
+
export const giteaConfig = {
baseUrl: rawGiteaBaseUrl,
hasToken: giteaToken.length > 0,
@@ -24,16 +64,34 @@ export const giteaConfig = {
isConfigured: rawGiteaBaseUrl.length > 0 && giteaToken.length > 0
};
-function getApiBaseUrl(baseUrl: string): string {
+export function getApiBaseUrl(baseUrl: string): string {
const normalized = baseUrl.replace(/\/+$/, "");
return normalized.endsWith("/api/v1") ? normalized : `${normalized}/api/v1`;
}
-export async function getCurrentGiteaUser(): Promise {
+function assertGiteaConfigured(): void {
if (!giteaConfig.isConfigured) {
throw new Error("Set VITE_GITEA_BASE_URL and VITE_GITEA_TOKEN before testing the connection.");
}
+}
+function createGiteaClient() {
+ assertGiteaConfigured();
+ return axios.create({
+ baseURL: getApiBaseUrl(giteaConfig.baseUrl),
+ headers: {
+ Authorization: `token ${giteaToken}`
+ }
+ });
+}
+
+export async function getCurrentGiteaUser(): Promise {
+ const client = createGiteaClient();
+ const response = await client.get("/user");
+ return response.data;
+}
+
+export async function getReposOrderedByLastCommit(): Promise {
const client = axios.create({
baseURL: getApiBaseUrl(giteaConfig.baseUrl),
headers: {
@@ -41,6 +99,62 @@ export async function getCurrentGiteaUser(): Promise {
}
});
- const response = await client.get("/user");
- return response.data;
+ assertGiteaConfigured();
+
+ const repos: GiteaRepoResponse[] = [];
+ let page = 1;
+
+ while (page <= 10) {
+ const response = await client.get("/user/repos", {
+ params: {
+ limit: 50,
+ page
+ }
+ });
+
+ repos.push(...response.data);
+
+ if (response.data.length < 50) {
+ break;
+ }
+
+ page += 1;
+ }
+
+ const repoDetails = await Promise.all(
+ repos.map(async (repo): Promise => {
+ 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(
+ `/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);
+ });
}
diff --git a/src/localContext.ts b/src/localContext.ts
new file mode 100644
index 0000000..19d6463
--- /dev/null
+++ b/src/localContext.ts
@@ -0,0 +1,50 @@
+export interface LocalGiteaRepoLink {
+ fullName: string;
+ webUrl: string;
+}
+
+export interface WorkspaceRepoLink extends LocalGiteaRepoLink {
+ worktreePath: string;
+ worktreeRelativePath: string;
+}
+
+export interface LocalWorkspace {
+ name: string;
+ path: string;
+ linkedRepos: WorkspaceRepoLink[];
+}
+
+export interface LocalGitWorktree {
+ name: string;
+ path: string;
+ relativePath: string;
+ topWorkspaceName: string;
+ branch: string | null;
+ giteaRepo: LocalGiteaRepoLink | null;
+}
+
+export interface KnownUrl {
+ label: string;
+ url: string;
+ workspaceName: string;
+ workspacePath: string;
+ giteaRepo: LocalGiteaRepoLink | null;
+}
+
+export interface LocalContext {
+ generatedAt: string;
+ workspaceRoot: string;
+ workspaces: LocalWorkspace[];
+ gitWorktrees: LocalGitWorktree[];
+ knownUrls: KnownUrl[];
+}
+
+export async function loadLocalContext(): Promise {
+ const response = await fetch("/local-context.json", { cache: "no-store" });
+
+ if (!response.ok) {
+ throw new Error(`Could not load local context (${response.status}). Run npm run build.`);
+ }
+
+ return response.json() as Promise;
+}
diff --git a/src/main.ts b/src/main.ts
index 215d5a1..c1e361b 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -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("#app-window");
+const dragHandle = document.querySelector("#window-drag-handle");
const configBadge = document.querySelector("#config-badge");
-const baseUrl = document.querySelector("#base-url");
-const tokenState = document.querySelector("#token-state");
-const targetRepo = document.querySelector("#target-repo");
const status = document.querySelector("#status");
-const testConnection = document.querySelector("#test-connection");
+const refreshData = document.querySelector("#refresh-data");
+const viewHeader = document.querySelector("#view-header");
+const viewContent = document.querySelector("#view-content");
+const viewButtons = Array.from(document.querySelectorAll("[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 = {
+ "&": "&",
+ "<": "<",
+ ">": ">",
+ "\"": """,
+ "'": "'"
+ };
+ 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 `No workspace link `;
+ }
+
+ return `Workspace link ${escapeHtml(worktree.path)} `;
+}
+
+function renderRepos(repos: GiteaRepoSummary[]): string {
+ if (!repos.length) {
+ return `No Gitea repos loaded yet.
`;
+ }
+
+ return repos
+ .map(
+ (repo) => `
+
+
+
+
${repo.isPrivate ? "Private" : "Public"}
+
+
+ Branch: ${escapeHtml(repo.defaultBranch)}
+ Last commit: ${formatDate(repo.lastCommitAt)}
+ SHA: ${formatSha(repo.lastCommitSha)}
+
+ ${escapeHtml(repo.lastCommitMessage)}
+ ${getRepoWorkspaceLink(repo)}
+
+ `
+ )
+ .join("");
+}
+
+function renderWorkspaces(workspaces: LocalWorkspace[]): string {
+ if (!workspaces.length) {
+ return `No workspaces found in workspace_Father.
`;
+ }
+
+ return workspaces
+ .map((workspace) => {
+ const links = workspace.linkedRepos.length
+ ? workspace.linkedRepos
+ .map(
+ (repo) =>
+ `${escapeHtml(repo.fullName)} `
+ )
+ .join("")
+ : `No Gitea repo link established `;
+
+ return `
+
+
+
+
${escapeHtml(workspace.name)}
+
${escapeHtml(workspace.path)}
+
+ ${workspace.linkedRepos.length ? `
Linked ` : ""}
+
+ ${links}
+
+ `;
+ })
+ .join("");
+}
+
+function renderUrls(urls: KnownUrl[]): string {
+ if (!urls.length) {
+ return `No known URLs registered.
`;
+ }
+
+ return urls
+ .map(
+ (knownUrl) => `
+
+
+
+ Workspace link
+ ${escapeHtml(knownUrl.workspacePath)}
+
+
+
+ `
+ )
+ .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 = {
+ urls: "URLs",
+ repos: "Gitea repos",
+ workspaces: "Workspaces"
+ };
+
+ const descriptions: Record = {
+ 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 = `
+
+
${titles[state.activeView]}
+
${escapeHtml(descriptions[state.activeView])}
+
+ `;
+
+ 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("[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();
diff --git a/src/store.ts b/src/store.ts
index c90af15..371bc92 100644
--- a/src/store.ts
+++ b/src/store.ts
@@ -1,23 +1,80 @@
import { createStore } from "zustand/vanilla";
-import { getCurrentGiteaUser, giteaConfig, type GiteaUser } from "./api";
+import {
+ getCurrentGiteaUser,
+ getReposOrderedByLastCommit,
+ giteaConfig,
+ type GiteaRepoSummary,
+ type GiteaUser
+} from "./api";
+import { loadLocalContext, type LocalContext } from "./localContext";
export type StatusTone = "neutral" | "success" | "error";
+export type AppView = "urls" | "repos" | "workspaces";
+
+export interface WindowPosition {
+ x: number;
+ y: number;
+}
interface AppState {
+ activeView: AppView;
config: typeof giteaConfig;
connectedUser: GiteaUser | null;
+ repos: GiteaRepoSummary[];
+ localContext: LocalContext | null;
+ windowPosition: WindowPosition;
+ isLoadingData: boolean;
isTestingConnection: boolean;
statusMessage: string;
statusTone: StatusTone;
+ setActiveView: (view: AppView) => void;
+ setWindowPosition: (position: WindowPosition) => void;
+ loadData: () => Promise;
testConnection: () => Promise;
}
export const appStore = createStore()((set) => ({
+ activeView: "repos",
config: giteaConfig,
connectedUser: null,
+ repos: [],
+ localContext: null,
+ windowPosition: { x: 10, y: 10 },
+ isLoadingData: false,
isTestingConnection: false,
statusMessage: "Ready.",
statusTone: "neutral",
+ setActiveView: (view) => set({ activeView: view }),
+ setWindowPosition: (position) => set({ windowPosition: position }),
+ loadData: async () => {
+ set({
+ isLoadingData: true,
+ statusMessage: "Loading workspace and Gitea context...",
+ statusTone: "neutral"
+ });
+
+ try {
+ const [localContext, repos] = await Promise.all([
+ loadLocalContext(),
+ getReposOrderedByLastCommit()
+ ]);
+
+ set({
+ localContext,
+ repos,
+ statusMessage: `Loaded ${repos.length} repos and ${localContext.workspaces.length} workspaces.`,
+ statusTone: "success"
+ });
+ } catch (error) {
+ const message = error instanceof Error ? error.message : "Could not load app data.";
+ set({
+ statusMessage: message,
+ statusTone: "error"
+ });
+ } finally {
+ set({ isLoadingData: false });
+ }
+ },
testConnection: async () => {
set({
connectedUser: null,
diff --git a/src/styles.css b/src/styles.css
index d732d4a..055b6fc 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -1,6 +1,6 @@
:root {
color: #1f2933;
- background: #f5f7fa;
+ background: #e7edf3;
font-family:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
sans-serif;
@@ -13,37 +13,75 @@
}
body {
- min-width: 360px;
+ width: 720px;
+ min-width: 720px;
+ height: 620px;
margin: 0;
- background: #f5f7fa;
+ overflow: hidden;
+ background:
+ linear-gradient(135deg, rgb(255 255 255 / 72%), rgb(218 228 238 / 72%)),
+ #e7edf3;
}
-.app-shell {
+button,
+a {
+ -webkit-tap-highlight-color: transparent;
+}
+
+.desktop-canvas {
+ position: relative;
+ width: 100vw;
+ height: 100vh;
+}
+
+.app-window {
+ position: absolute;
display: grid;
- gap: 14px;
- padding: 16px;
+ grid-template-rows: auto auto 1fr auto;
+ width: min(700px, calc(100vw - 20px));
+ height: min(600px, calc(100vh - 20px));
+ overflow: hidden;
+ border: 1px solid #bcccdc;
+ border-radius: 8px;
+ background: #f8fafc;
+ box-shadow: 0 18px 48px rgb(16 42 67 / 18%);
}
-.masthead {
+.window-titlebar {
display: flex;
- align-items: flex-start;
+ align-items: center;
justify-content: space-between;
- gap: 14px;
+ gap: 16px;
+ padding: 12px 14px;
+ border-bottom: 1px solid #d9e2ec;
+ background: #ffffff;
+ cursor: grab;
+ user-select: none;
+ touch-action: none;
+}
+
+.window-titlebar:active {
+ cursor: grabbing;
}
.eyebrow {
- margin: 0 0 4px;
+ margin: 0 0 2px;
color: #52616f;
- font-size: 12px;
+ font-size: 11px;
font-weight: 700;
letter-spacing: 0;
text-transform: uppercase;
}
-h1 {
+h1,
+h2,
+p {
margin: 0;
+}
+
+h1 {
color: #102a43;
- font-size: 22px;
+ font-size: 20px;
line-height: 1.15;
}
@@ -70,64 +108,244 @@ h1 {
color: #8d6e00;
}
-.panel {
+.badge.quiet {
+ border-color: #d9e2ec;
+ background: #f5f7fa;
+ color: #52616f;
+}
+
+.toolbar {
display: grid;
- gap: 14px;
- border: 1px solid #d9e2ec;
- border-radius: 8px;
- padding: 14px;
+ grid-template-columns: repeat(5, minmax(0, 1fr));
+ gap: 1px;
+ border-bottom: 1px solid #d9e2ec;
+ background: #d9e2ec;
+}
+
+.tool-button,
+.icon-button,
+.visit-button {
+ border: 0;
+ font: inherit;
+}
+
+.tool-button {
+ display: grid;
+ justify-items: center;
+ gap: 4px;
+ min-width: 0;
+ min-height: 58px;
+ padding: 8px 6px;
background: #ffffff;
- box-shadow: 0 8px 24px rgb(16 42 67 / 8%);
+ color: #334e68;
+ cursor: pointer;
}
-.config-list {
+.tool-button svg {
+ width: 19px;
+ height: 19px;
+ stroke-width: 2.25;
+}
+
+.tool-button span {
+ max-width: 100%;
+ overflow: hidden;
+ font-size: 11px;
+ font-weight: 700;
+ line-height: 1.15;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.tool-button[data-active="true"] {
+ background: #e6f6ff;
+ color: #075985;
+}
+
+.tool-button:disabled {
+ cursor: not-allowed;
+ opacity: 0.45;
+}
+
+.content-panel {
display: grid;
- gap: 10px;
- margin: 0;
+ grid-template-rows: auto 1fr;
+ min-height: 0;
}
-.config-list div {
- display: grid;
- gap: 3px;
+.view-header {
+ padding: 14px 16px 10px;
+ border-bottom: 1px solid #edf2f7;
+ background: #f8fafc;
}
-dt {
+.view-header h2 {
+ color: #102a43;
+ font-size: 17px;
+ line-height: 1.2;
+}
+
+.view-header p {
+ margin-top: 3px;
color: #52616f;
font-size: 12px;
- font-weight: 700;
+ line-height: 1.4;
}
-dd {
- margin: 0;
- overflow-wrap: anywhere;
+.view-content {
+ display: grid;
+ align-content: start;
+ gap: 10px;
+ min-height: 0;
+ overflow-y: auto;
+ padding: 12px;
+}
+
+.item-card {
+ display: grid;
+ gap: 9px;
+ border: 1px solid #d9e2ec;
+ border-radius: 8px;
+ padding: 12px;
+ background: #ffffff;
+}
+
+.item-heading {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 12px;
+}
+
+.item-title {
color: #102a43;
- font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
- font-size: 13px;
+ font-size: 14px;
+ font-weight: 800;
+ line-height: 1.25;
+ text-decoration: none;
}
-button {
- width: 100%;
- border: 0;
+a.item-title:hover,
+.repo-link:hover,
+.url-text:hover {
+ text-decoration: underline;
+}
+
+.item-subtitle {
+ margin-top: 3px;
+ color: #52616f;
+ font-size: 12px;
+ line-height: 1.4;
+}
+
+.meta-grid {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 8px;
+ color: #52616f;
+ font-size: 11px;
+}
+
+.meta-grid strong {
+ color: #243b53;
+ font-weight: 800;
+}
+
+.commit-message {
+ color: #243b53;
+ font-size: 12px;
+ line-height: 1.4;
+}
+
+.linked-row {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 7px;
+ color: #52616f;
+ font-size: 12px;
+}
+
+.link-chip {
+ display: inline-flex;
+ align-items: center;
+ border: 1px solid #9fb3c8;
+ border-radius: 999px;
+ padding: 2px 7px;
+ background: #f0f4f8;
+ color: #334e68;
+ font-size: 11px;
+ font-weight: 800;
+}
+
+.path-text {
+ overflow-wrap: anywhere;
+ color: #334e68;
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
+ font-size: 11px;
+ line-height: 1.35;
+}
+
+.repo-link-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 7px;
+}
+
+.repo-link,
+.url-text {
+ overflow-wrap: anywhere;
+ color: #0b6bcb;
+ font-size: 12px;
+ font-weight: 700;
+ text-decoration: none;
+}
+
+.visit-button {
+ flex: 0 0 auto;
border-radius: 6px;
- padding: 10px 12px;
+ padding: 7px 10px;
background: #0b6bcb;
color: #ffffff;
cursor: pointer;
- font: inherit;
- font-weight: 700;
+ font-size: 12px;
+ font-weight: 800;
}
-button:disabled {
- cursor: wait;
- opacity: 0.7;
+.empty-state {
+ border: 1px dashed #bcccdc;
+ border-radius: 8px;
+ padding: 18px;
+ color: #52616f;
+ background: #ffffff;
+ font-size: 13px;
+ text-align: center;
+}
+
+.muted {
+ color: #829ab1;
+ font-size: 12px;
+}
+
+.statusbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ min-height: 42px;
+ border-top: 1px solid #d9e2ec;
+ padding: 8px 10px 8px 14px;
+ background: #ffffff;
}
#status {
- min-height: 20px;
- margin: 0;
+ min-width: 0;
+ overflow: hidden;
color: #52616f;
- font-size: 13px;
+ font-size: 12px;
line-height: 1.4;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
#status[data-tone="success"] {
@@ -137,3 +355,36 @@ button:disabled {
#status[data-tone="error"] {
color: #b42318;
}
+
+.icon-button {
+ display: inline-grid;
+ flex: 0 0 auto;
+ place-items: center;
+ width: 30px;
+ height: 30px;
+ border-radius: 6px;
+ background: #eef4f9;
+ color: #334e68;
+ cursor: pointer;
+}
+
+.icon-button:disabled {
+ cursor: wait;
+ opacity: 0.65;
+}
+
+.icon-button svg {
+ width: 16px;
+ height: 16px;
+}
+
+@media (max-width: 520px) {
+ body {
+ width: 420px;
+ min-width: 420px;
+ }
+
+ .meta-grid {
+ grid-template-columns: 1fr;
+ }
+}