This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
+38
-21
@@ -7,33 +7,50 @@
|
||||
<title>Silma AI Aide</title>
|
||||
</head>
|
||||
<body>
|
||||
<main class="app-shell">
|
||||
<header class="masthead">
|
||||
<main class="desktop-canvas">
|
||||
<section id="app-window" class="app-window" aria-label="Silma AI Aide window">
|
||||
<header id="window-drag-handle" class="window-titlebar">
|
||||
<div>
|
||||
<p class="eyebrow">Gitea</p>
|
||||
<h1>Silma AI Aide</h1>
|
||||
<p class="eyebrow">Silma</p>
|
||||
<h1>AI Aide</h1>
|
||||
</div>
|
||||
<span id="config-badge" class="badge">Checking</span>
|
||||
</header>
|
||||
|
||||
<section class="panel">
|
||||
<dl class="config-list">
|
||||
<div>
|
||||
<dt>Base URL</dt>
|
||||
<dd id="base-url">Not configured</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Token</dt>
|
||||
<dd id="token-state">Not configured</dd>
|
||||
</div>
|
||||
<div>
|
||||
<dt>Target repo</dt>
|
||||
<dd id="target-repo">Not configured</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<nav class="toolbar" aria-label="Aide sections">
|
||||
<button class="tool-button" type="button" data-action="add-url" disabled title="Add URL">
|
||||
<i data-lucide="link"></i>
|
||||
<span>Add URL</span>
|
||||
</button>
|
||||
<button class="tool-button" type="button" data-view="urls" title="URLs">
|
||||
<i data-lucide="list"></i>
|
||||
<span>URLs</span>
|
||||
</button>
|
||||
<button class="tool-button" type="button" data-view="repos" title="Gitea repos">
|
||||
<i data-lucide="git-branch"></i>
|
||||
<span>Gitea Repos</span>
|
||||
</button>
|
||||
<button class="tool-button" type="button" data-view="workspaces" title="Workspaces">
|
||||
<i data-lucide="folder-git-2"></i>
|
||||
<span>Workspaces</span>
|
||||
</button>
|
||||
<button class="tool-button" type="button" data-action="add-issue" disabled title="Add issue">
|
||||
<i data-lucide="circle-plus"></i>
|
||||
<span>Add Issue</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<button id="test-connection" type="button">Test connection</button>
|
||||
<p id="status" role="status">Ready.</p>
|
||||
<section class="content-panel">
|
||||
<div id="view-header" class="view-header"></div>
|
||||
<div id="view-content" class="view-content"></div>
|
||||
</section>
|
||||
|
||||
<footer class="statusbar">
|
||||
<span id="status" role="status">Ready.</span>
|
||||
<button id="refresh-data" class="icon-button" type="button" title="Refresh data">
|
||||
<i data-lucide="refresh-cw"></i>
|
||||
</button>
|
||||
</footer>
|
||||
</section>
|
||||
</main>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
Generated
+7
@@ -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",
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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`);
|
||||
+118
-4
@@ -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<GiteaUser> {
|
||||
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<GiteaUser> {
|
||||
const client = createGiteaClient();
|
||||
const response = await client.get<GiteaUser>("/user");
|
||||
return response.data;
|
||||
}
|
||||
|
||||
export async function getReposOrderedByLastCommit(): Promise<GiteaRepoSummary[]> {
|
||||
const client = axios.create({
|
||||
baseURL: getApiBaseUrl(giteaConfig.baseUrl),
|
||||
headers: {
|
||||
@@ -41,6 +99,62 @@ export async function getCurrentGiteaUser(): Promise<GiteaUser> {
|
||||
}
|
||||
});
|
||||
|
||||
const response = await client.get<GiteaUser>("/user");
|
||||
return response.data;
|
||||
assertGiteaConfigured();
|
||||
|
||||
const repos: GiteaRepoResponse[] = [];
|
||||
let page = 1;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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<LocalContext> {
|
||||
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<LocalContext>;
|
||||
}
|
||||
+287
-21
@@ -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> = {
|
||||
"&": "&",
|
||||
"<": "<",
|
||||
">": ">",
|
||||
"\"": """,
|
||||
"'": "'"
|
||||
};
|
||||
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();
|
||||
|
||||
+58
-1
@@ -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<void>;
|
||||
testConnection: () => Promise<void>;
|
||||
}
|
||||
|
||||
export const appStore = createStore<AppState>()((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,
|
||||
|
||||
+295
-44
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user