diff --git a/.gitignore b/.gitignore
index a62cc11..6d203d1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
.idea/
.core/
docker/.env
+ui/node_modules
diff --git a/ui/dist/agent-panel.d.ts b/ui/dist/agent-panel.d.ts
new file mode 100644
index 0000000..753fd72
--- /dev/null
+++ b/ui/dist/agent-panel.d.ts
@@ -0,0 +1,23 @@
+import { LitElement } from 'lit';
+/**
+ * Agent dashboard panel — shows issues, sprint progress, and fleet status.
+ * Works in core/ide (Wails), lthn.sh (Laravel), and standalone browsers.
+ *
+ * @element core-agent-panel
+ */
+export declare class CoreAgentPanel extends LitElement {
+ apiUrl: string;
+ apiKey: string;
+ private issues;
+ private sprint;
+ private loading;
+ private error;
+ private activeTab;
+ static styles: import("lit").CSSResult;
+ connectedCallback(): void;
+ private fetchData;
+ private setTab;
+ private renderIssues;
+ private renderSprint;
+ render(): import("lit-html").TemplateResult<1>;
+}
diff --git a/ui/dist/agent-panel.js b/ui/dist/agent-panel.js
new file mode 100644
index 0000000..639b458
--- /dev/null
+++ b/ui/dist/agent-panel.js
@@ -0,0 +1,324 @@
+var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
+};
+import { LitElement, html, css } from 'lit';
+import { customElement, property, state } from 'lit/decorators.js';
+/**
+ * Agent dashboard panel — shows issues, sprint progress, and fleet status.
+ * Works in core/ide (Wails), lthn.sh (Laravel), and standalone browsers.
+ *
+ * @element core-agent-panel
+ */
+let CoreAgentPanel = class CoreAgentPanel extends LitElement {
+ constructor() {
+ super(...arguments);
+ this.apiUrl = '';
+ this.apiKey = '';
+ this.issues = [];
+ this.sprint = null;
+ this.loading = true;
+ this.error = '';
+ this.activeTab = 'issues';
+ }
+ static { this.styles = css `
+ :host {
+ display: block;
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
+ color: #e2e8f0;
+ background: #0f172a;
+ border-radius: 0.75rem;
+ overflow: hidden;
+ }
+
+ .header {
+ display: flex;
+ align-items: centre;
+ justify-content: space-between;
+ padding: 1rem 1.25rem;
+ background: #1e293b;
+ border-bottom: 1px solid #334155;
+ }
+
+ .header h2 {
+ margin: 0;
+ font-size: 1rem;
+ font-weight: 600;
+ color: #f1f5f9;
+ }
+
+ .tabs {
+ display: flex;
+ gap: 0.25rem;
+ background: #0f172a;
+ border-radius: 0.375rem;
+ padding: 0.125rem;
+ }
+
+ .tab {
+ padding: 0.375rem 0.75rem;
+ font-size: 0.75rem;
+ font-weight: 500;
+ border: none;
+ background: transparent;
+ color: #94a3b8;
+ border-radius: 0.25rem;
+ cursor: pointer;
+ transition: all 0.15s;
+ }
+
+ .tab.active {
+ background: #334155;
+ color: #f1f5f9;
+ }
+
+ .tab:hover:not(.active) {
+ color: #cbd5e1;
+ }
+
+ .content {
+ padding: 1rem 1.25rem;
+ max-height: 400px;
+ overflow-y: auto;
+ }
+
+ .issue-row {
+ display: flex;
+ align-items: centre;
+ justify-content: space-between;
+ padding: 0.625rem 0;
+ border-bottom: 1px solid #1e293b;
+ }
+
+ .issue-row:last-child {
+ border-bottom: none;
+ }
+
+ .issue-title {
+ font-size: 0.875rem;
+ color: #e2e8f0;
+ flex: 1;
+ margin-right: 0.75rem;
+ }
+
+ .badge {
+ display: inline-block;
+ padding: 0.125rem 0.5rem;
+ border-radius: 9999px;
+ font-size: 0.625rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.025em;
+ }
+
+ .badge-open { background: #1e3a5f; color: #60a5fa; }
+ .badge-assigned { background: #3b2f63; color: #a78bfa; }
+ .badge-in_progress { background: #422006; color: #f59e0b; }
+ .badge-review { background: #164e63; color: #22d3ee; }
+ .badge-done { background: #14532d; color: #4ade80; }
+ .badge-closed { background: #1e293b; color: #64748b; }
+
+ .badge-critical { background: #450a0a; color: #ef4444; }
+ .badge-high { background: #431407; color: #f97316; }
+ .badge-normal { background: #1e293b; color: #94a3b8; }
+ .badge-low { background: #1e293b; color: #64748b; }
+
+ .sprint-card {
+ background: #1e293b;
+ border-radius: 0.5rem;
+ padding: 1.25rem;
+ }
+
+ .sprint-title {
+ font-size: 1rem;
+ font-weight: 600;
+ margin-bottom: 0.75rem;
+ }
+
+ .progress-bar {
+ height: 0.5rem;
+ background: #334155;
+ border-radius: 9999px;
+ overflow: hidden;
+ margin-bottom: 0.5rem;
+ }
+
+ .progress-fill {
+ height: 100%;
+ background: linear-gradient(90deg, #8b5cf6, #6366f1);
+ border-radius: 9999px;
+ transition: width 0.3s ease;
+ }
+
+ .progress-stats {
+ display: flex;
+ gap: 1rem;
+ font-size: 0.75rem;
+ color: #94a3b8;
+ }
+
+ .stat {
+ display: flex;
+ align-items: centre;
+ gap: 0.25rem;
+ }
+
+ .stat-value {
+ font-weight: 600;
+ color: #e2e8f0;
+ }
+
+ .empty {
+ text-align: centre;
+ padding: 2rem;
+ color: #64748b;
+ font-size: 0.875rem;
+ }
+
+ .error {
+ text-align: centre;
+ padding: 1rem;
+ color: #ef4444;
+ font-size: 0.875rem;
+ }
+
+ .loading {
+ text-align: centre;
+ padding: 2rem;
+ color: #64748b;
+ }
+ `; }
+ connectedCallback() {
+ super.connectedCallback();
+ this.fetchData();
+ // Refresh every 30 seconds
+ setInterval(() => this.fetchData(), 30000);
+ }
+ async fetchData() {
+ const base = this.apiUrl || window.location.origin;
+ const headers = {
+ 'Accept': 'application/json',
+ };
+ if (this.apiKey) {
+ headers['Authorization'] = `Bearer ${this.apiKey}`;
+ }
+ try {
+ const [issuesRes, sprintsRes] = await Promise.all([
+ fetch(`${base}/v1/issues`, { headers }),
+ fetch(`${base}/v1/sprints`, { headers }),
+ ]);
+ if (issuesRes.ok) {
+ const issuesData = await issuesRes.json();
+ this.issues = issuesData.data || [];
+ }
+ if (sprintsRes.ok) {
+ const sprintsData = await sprintsRes.json();
+ const sprints = sprintsData.data || [];
+ this.sprint = sprints.find((s) => s.status === 'active') || sprints[0] || null;
+ }
+ this.loading = false;
+ this.error = '';
+ }
+ catch (e) {
+ this.error = 'Failed to connect to API';
+ this.loading = false;
+ }
+ }
+ setTab(tab) {
+ this.activeTab = tab;
+ }
+ renderIssues() {
+ if (this.issues.length === 0) {
+ return html `
No issues found
`;
+ }
+ return this.issues.map(issue => html `
+
+ ${issue.title}
+ ${issue.priority}
+ ${issue.status}
+
+ `);
+ }
+ renderSprint() {
+ if (!this.sprint) {
+ return html `No active sprint
`;
+ }
+ const progress = this.sprint.progress;
+ return html `
+
+
${this.sprint.title}
+
${this.sprint.status}
+
+
+
+ ${progress.total} total
+
+
+ ${progress.open} open
+
+
+ ${progress.in_progress} in progress
+
+
+ ${progress.closed} done
+
+
+
+ `;
+ }
+ render() {
+ if (this.loading) {
+ return html `Loading...
`;
+ }
+ if (this.error) {
+ return html `${this.error}
`;
+ }
+ return html `
+
+
+ ${this.activeTab === 'issues' ? this.renderIssues() : this.renderSprint()}
+
+ `;
+ }
+};
+__decorate([
+ property({ type: String, attribute: 'api-url' })
+], CoreAgentPanel.prototype, "apiUrl", void 0);
+__decorate([
+ property({ type: String, attribute: 'api-key' })
+], CoreAgentPanel.prototype, "apiKey", void 0);
+__decorate([
+ state()
+], CoreAgentPanel.prototype, "issues", void 0);
+__decorate([
+ state()
+], CoreAgentPanel.prototype, "sprint", void 0);
+__decorate([
+ state()
+], CoreAgentPanel.prototype, "loading", void 0);
+__decorate([
+ state()
+], CoreAgentPanel.prototype, "error", void 0);
+__decorate([
+ state()
+], CoreAgentPanel.prototype, "activeTab", void 0);
+CoreAgentPanel = __decorate([
+ customElement('core-agent-panel')
+], CoreAgentPanel);
+export { CoreAgentPanel };
diff --git a/ui/dist/index.html b/ui/dist/index.html
new file mode 100644
index 0000000..22afe08
--- /dev/null
+++ b/ui/dist/index.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+ Core Agent Dashboard
+
+
+
+
+
+
+
+
diff --git a/ui/index.html b/ui/index.html
new file mode 100644
index 0000000..22afe08
--- /dev/null
+++ b/ui/index.html
@@ -0,0 +1,23 @@
+
+
+
+
+
+ Core Agent Dashboard
+
+
+
+
+
+
+
+
diff --git a/ui/package-lock.json b/ui/package-lock.json
new file mode 100644
index 0000000..ff72595
--- /dev/null
+++ b/ui/package-lock.json
@@ -0,0 +1,84 @@
+{
+ "name": "core-agent-panel",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "core-agent-panel",
+ "version": "0.1.0",
+ "dependencies": {
+ "lit": "^3.0.0"
+ },
+ "devDependencies": {
+ "typescript": "^5.0.0"
+ }
+ },
+ "node_modules/@lit-labs/ssr-dom-shim": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz",
+ "integrity": "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/@lit/reactive-element": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz",
+ "integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@lit-labs/ssr-dom-shim": "^1.5.0"
+ }
+ },
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT"
+ },
+ "node_modules/lit": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.2.tgz",
+ "integrity": "sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@lit/reactive-element": "^2.1.0",
+ "lit-element": "^4.2.0",
+ "lit-html": "^3.3.0"
+ }
+ },
+ "node_modules/lit-element": {
+ "version": "4.2.2",
+ "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz",
+ "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@lit-labs/ssr-dom-shim": "^1.5.0",
+ "@lit/reactive-element": "^2.1.0",
+ "lit-html": "^3.3.0"
+ }
+ },
+ "node_modules/lit-html": {
+ "version": "3.3.2",
+ "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.2.tgz",
+ "integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==",
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@types/trusted-types": "^2.0.2"
+ }
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ }
+ }
+}
diff --git a/ui/package.json b/ui/package.json
new file mode 100644
index 0000000..decb0be
--- /dev/null
+++ b/ui/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "core-agent-panel",
+ "version": "0.1.0",
+ "description": "Agent dashboard custom element — issues, sprint, fleet status",
+ "type": "module",
+ "scripts": {
+ "build": "tsc && cp index.html dist/",
+ "dev": "tsc --watch"
+ },
+ "dependencies": {
+ "lit": "^3.0.0"
+ },
+ "devDependencies": {
+ "typescript": "^5.0.0"
+ }
+}
diff --git a/ui/src/agent-panel.ts b/ui/src/agent-panel.ts
new file mode 100644
index 0000000..b22f1c4
--- /dev/null
+++ b/ui/src/agent-panel.ts
@@ -0,0 +1,336 @@
+import { LitElement, html, css } from 'lit';
+import { customElement, property, state } from 'lit/decorators.js';
+
+interface Issue {
+ slug: string;
+ title: string;
+ type: string;
+ status: string;
+ priority: string;
+ assignee: string | null;
+ labels: string[];
+ updated_at: string;
+}
+
+interface Sprint {
+ slug: string;
+ title: string;
+ status: string;
+ progress: {
+ total: number;
+ closed: number;
+ in_progress: number;
+ open: number;
+ percentage: number;
+ };
+ started_at: string | null;
+}
+
+/**
+ * Agent dashboard panel — shows issues, sprint progress, and fleet status.
+ * Works in core/ide (Wails), lthn.sh (Laravel), and standalone browsers.
+ *
+ * @element core-agent-panel
+ */
+@customElement('core-agent-panel')
+export class CoreAgentPanel extends LitElement {
+ @property({ type: String, attribute: 'api-url' })
+ apiUrl = '';
+
+ @property({ type: String, attribute: 'api-key' })
+ apiKey = '';
+
+ @state() private issues: Issue[] = [];
+ @state() private sprint: Sprint | null = null;
+ @state() private loading = true;
+ @state() private error = '';
+ @state() private activeTab: 'issues' | 'sprint' = 'issues';
+
+ static styles = css`
+ :host {
+ display: block;
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
+ color: #e2e8f0;
+ background: #0f172a;
+ border-radius: 0.75rem;
+ overflow: hidden;
+ }
+
+ .header {
+ display: flex;
+ align-items: centre;
+ justify-content: space-between;
+ padding: 1rem 1.25rem;
+ background: #1e293b;
+ border-bottom: 1px solid #334155;
+ }
+
+ .header h2 {
+ margin: 0;
+ font-size: 1rem;
+ font-weight: 600;
+ color: #f1f5f9;
+ }
+
+ .tabs {
+ display: flex;
+ gap: 0.25rem;
+ background: #0f172a;
+ border-radius: 0.375rem;
+ padding: 0.125rem;
+ }
+
+ .tab {
+ padding: 0.375rem 0.75rem;
+ font-size: 0.75rem;
+ font-weight: 500;
+ border: none;
+ background: transparent;
+ color: #94a3b8;
+ border-radius: 0.25rem;
+ cursor: pointer;
+ transition: all 0.15s;
+ }
+
+ .tab.active {
+ background: #334155;
+ color: #f1f5f9;
+ }
+
+ .tab:hover:not(.active) {
+ color: #cbd5e1;
+ }
+
+ .content {
+ padding: 1rem 1.25rem;
+ max-height: 400px;
+ overflow-y: auto;
+ }
+
+ .issue-row {
+ display: flex;
+ align-items: centre;
+ justify-content: space-between;
+ padding: 0.625rem 0;
+ border-bottom: 1px solid #1e293b;
+ }
+
+ .issue-row:last-child {
+ border-bottom: none;
+ }
+
+ .issue-title {
+ font-size: 0.875rem;
+ color: #e2e8f0;
+ flex: 1;
+ margin-right: 0.75rem;
+ }
+
+ .badge {
+ display: inline-block;
+ padding: 0.125rem 0.5rem;
+ border-radius: 9999px;
+ font-size: 0.625rem;
+ font-weight: 600;
+ text-transform: uppercase;
+ letter-spacing: 0.025em;
+ }
+
+ .badge-open { background: #1e3a5f; color: #60a5fa; }
+ .badge-assigned { background: #3b2f63; color: #a78bfa; }
+ .badge-in_progress { background: #422006; color: #f59e0b; }
+ .badge-review { background: #164e63; color: #22d3ee; }
+ .badge-done { background: #14532d; color: #4ade80; }
+ .badge-closed { background: #1e293b; color: #64748b; }
+
+ .badge-critical { background: #450a0a; color: #ef4444; }
+ .badge-high { background: #431407; color: #f97316; }
+ .badge-normal { background: #1e293b; color: #94a3b8; }
+ .badge-low { background: #1e293b; color: #64748b; }
+
+ .sprint-card {
+ background: #1e293b;
+ border-radius: 0.5rem;
+ padding: 1.25rem;
+ }
+
+ .sprint-title {
+ font-size: 1rem;
+ font-weight: 600;
+ margin-bottom: 0.75rem;
+ }
+
+ .progress-bar {
+ height: 0.5rem;
+ background: #334155;
+ border-radius: 9999px;
+ overflow: hidden;
+ margin-bottom: 0.5rem;
+ }
+
+ .progress-fill {
+ height: 100%;
+ background: linear-gradient(90deg, #8b5cf6, #6366f1);
+ border-radius: 9999px;
+ transition: width 0.3s ease;
+ }
+
+ .progress-stats {
+ display: flex;
+ gap: 1rem;
+ font-size: 0.75rem;
+ color: #94a3b8;
+ }
+
+ .stat {
+ display: flex;
+ align-items: centre;
+ gap: 0.25rem;
+ }
+
+ .stat-value {
+ font-weight: 600;
+ color: #e2e8f0;
+ }
+
+ .empty {
+ text-align: centre;
+ padding: 2rem;
+ color: #64748b;
+ font-size: 0.875rem;
+ }
+
+ .error {
+ text-align: centre;
+ padding: 1rem;
+ color: #ef4444;
+ font-size: 0.875rem;
+ }
+
+ .loading {
+ text-align: centre;
+ padding: 2rem;
+ color: #64748b;
+ }
+ `;
+
+ connectedCallback() {
+ super.connectedCallback();
+ this.fetchData();
+ // Refresh every 30 seconds
+ setInterval(() => this.fetchData(), 30000);
+ }
+
+ private async fetchData() {
+ const base = this.apiUrl || window.location.origin;
+ const headers: Record = {
+ 'Accept': 'application/json',
+ };
+ if (this.apiKey) {
+ headers['Authorization'] = `Bearer ${this.apiKey}`;
+ }
+
+ try {
+ const [issuesRes, sprintsRes] = await Promise.all([
+ fetch(`${base}/v1/issues`, { headers }),
+ fetch(`${base}/v1/sprints`, { headers }),
+ ]);
+
+ if (issuesRes.ok) {
+ const issuesData = await issuesRes.json();
+ this.issues = issuesData.data || [];
+ }
+
+ if (sprintsRes.ok) {
+ const sprintsData = await sprintsRes.json();
+ const sprints = sprintsData.data || [];
+ this.sprint = sprints.find((s: Sprint) => s.status === 'active') || sprints[0] || null;
+ }
+
+ this.loading = false;
+ this.error = '';
+ } catch (e) {
+ this.error = 'Failed to connect to API';
+ this.loading = false;
+ }
+ }
+
+ private setTab(tab: 'issues' | 'sprint') {
+ this.activeTab = tab;
+ }
+
+ private renderIssues() {
+ if (this.issues.length === 0) {
+ return html`No issues found
`;
+ }
+
+ return this.issues.map(issue => html`
+
+ ${issue.title}
+ ${issue.priority}
+ ${issue.status}
+
+ `);
+ }
+
+ private renderSprint() {
+ if (!this.sprint) {
+ return html`No active sprint
`;
+ }
+
+ const progress = this.sprint.progress;
+
+ return html`
+
+
${this.sprint.title}
+
${this.sprint.status}
+
+
+
+ ${progress.total} total
+
+
+ ${progress.open} open
+
+
+ ${progress.in_progress} in progress
+
+
+ ${progress.closed} done
+
+
+
+ `;
+ }
+
+ render() {
+ if (this.loading) {
+ return html`Loading...
`;
+ }
+
+ if (this.error) {
+ return html`${this.error}
`;
+ }
+
+ return html`
+
+
+ ${this.activeTab === 'issues' ? this.renderIssues() : this.renderSprint()}
+
+ `;
+ }
+}
diff --git a/ui/tsconfig.json b/ui/tsconfig.json
new file mode 100644
index 0000000..a919f24
--- /dev/null
+++ b/ui/tsconfig.json
@@ -0,0 +1,15 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ES2022",
+ "moduleResolution": "bundler",
+ "declaration": true,
+ "outDir": "dist",
+ "rootDir": "src",
+ "strict": true,
+ "experimentalDecorators": true,
+ "useDefineForClassFields": false,
+ "skipLibCheck": true
+ },
+ "include": ["src/**/*.ts"]
+}