Jelajahi Sumber

Initial commit: GH-Hub VS Code extension v0.2.0

GitHub management panel for VS Code — repos, issues, PRs, push, AI replies.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
GeneralNoCodeDP 3 bulan lalu
melakukan
93af9ec0e8
9 mengubah file dengan 2192 tambahan dan 0 penghapusan
  1. 4 0
      .gitignore
  2. 4 0
      .vscodeignore
  3. 233 0
      README-draft.md
  4. 58 0
      ai.js
  5. 299 0
      extension.js
  6. 215 0
      gh.js
  7. 913 0
      media/dashboard.html
  8. 438 0
      package-lock.json
  9. 28 0
      package.json

+ 4 - 0
.gitignore

@@ -0,0 +1,4 @@
+node_modules/
+.env
+.DS_Store
+*.vsix

+ 4 - 0
.vscodeignore

@@ -0,0 +1,4 @@
+.env
+.gitignore
+.DS_Store
+*.vsix

+ 233 - 0
README-draft.md

@@ -0,0 +1,233 @@
+<p align="center">
+  <img src="https://raw.githubusercontent.com/VisualLogic-AI/VisualLogic.ai-VL/main/assets/logo.png" alt="VisualLogic.ai" width="420"/>
+</p>
+
+<h1 align="center">VisualLogic.ai — The AI-Native Programming System</h1>
+
+<p align="center">
+  <strong>Where Language, Visual Graphs, and AI Agents are fully reversible.</strong><br/>
+  Build production-ready applications from natural language — deterministically.
+</p>
+
+<p align="center">
+  <a href="https://editor.visuallogic.ai/">Website</a> &nbsp;|&nbsp;
+  <a href="https://www.youtube.com/playlist?list=PLJE6c8wBknRnCZIRv_VFa1dYswTSqoW21">YouTube</a> &nbsp;|&nbsp;
+  <a href="https://discord.com/invite/KdaVtR7pzv">Discord</a>
+</p>
+
+---
+
+## What is VisualLogic?
+
+VisualLogic is a **full-stack AI programming system** built on **VL (Visual Language)** — a minimal, deterministic language designed specifically for AI code generation.
+
+It is **not** a low-code tool. It is **not** a prompt wrapper. It is a complete programming system where:
+
+- **AI generates code** with near-perfect accuracy, thanks to VL's low-entropy grammar
+- **Visual graphs** provide a lossless, bidirectional view of every program
+- **Multi-agent workflows** orchestrate the entire development lifecycle
+
+> VisualLogic is not a tool on top of code. VisualLogic *is the programming system itself.*
+
+<p align="center">
+  <img src="https://raw.githubusercontent.com/VisualLogic-AI/VisualLogic.ai-VL/main/assets/images/vl-code-editor.png" alt="VL-Code Editor — AI-powered code editing with VL syntax" width="90%"/>
+  <br/>
+  <em>VL-Code Editor — AI-powered code editing with full VL syntax highlighting</em>
+</p>
+
+---
+
+## The VL Language
+
+VL (Visual Language) v2.91 is a declarative, component-oriented language covering the full stack through six file types:
+
+| Extension | Purpose | Description |
+|-----------|---------|-------------|
+| `.vx` | App | Navigation, routing, configuration |
+| `.sc` | Section | UI tree, state, events, styles |
+| `.cp` | Component | Reusable props-driven UI blocks |
+| `.vs` | Service | API contracts, domain logic |
+| `.vdb` | Database | Tables, columns, types, relations |
+| `.vth` | Theme | Colors, typography, spacing tokens |
+
+### Why VL?
+
+Traditional programming languages carry enormous entropy — formatting choices, import orderings, naming conventions, bracket styles. This makes AI generation unreliable and expensive.
+
+VL eliminates this problem:
+
+- **Stable grammar** — no formatting ambiguity, no hidden state
+- **Dash-based tree structure** — clean hierarchy without brace/bracket noise
+- **Ultra-low token cost** — compact representation means faster, cheaper AI generation
+- **Massive parallelism** — independent components enable multi-agent concurrent generation
+
+```
+APP MyApp
+--title: My Application
+--theme: Theme-Default
+
+SECTION Dashboard
+--$userCount(INT) = 0
+
+--HANDLER onLoad()
+----CALL UserDomain.getStats()
+------ON SUCCESS
+--------SET $userCount = result.total
+
+--<Column "root">
+----<StatCard "users"> label:"Total Users" value:$userCount
+```
+
+---
+
+## How It Works
+
+### Natural Language to Production App
+
+Give VisualLogic a product requirement in plain language. An automated **8-agent pipeline** handles everything:
+
+| # | Agent | Output |
+|---|-------|--------|
+| 1 | Requirements Analyst | PRD document |
+| 2 | Data Architect | Database schema (`.vdb`) |
+| 3 | Service Designer | API contracts (`.vs`) |
+| 4 | UI Architect | Screen/component breakdown |
+| 5 | Component Builder | Reusable UI components (`.cp`) |
+| 6 | Service Coder | Business logic (`.vs`) |
+| 7 | Screen Builder | Screens with state & events (`.sc`) |
+| 8 | App Assembler | App with routing (`.vx`) |
+
+Stages 5-8 run **in parallel** using independent agents. The result: a fully compilable, deployable application.
+
+### Bidirectional Code-Graph Transformation
+
+Every VL program has a direct, lossless mapping to a visual graph and back:
+
+```
+Natural Language
+       |
+   AI IDE (Agents + Workflows)
+       |
+   VL Source Files (.vx .sc .vs .vdb)
+       |
+   Visual IDE (Graph Editor)
+       |
+   VL Compiler
+       |
+   JS + Java / Node.js (Full Stack)
+```
+
+Edit in code. Switch to the visual canvas. Drag, connect, rearrange. Switch back. **Nothing is lost.** This is the "graphical escape hatch" that makes VisualLogic accessible to everyone — from senior engineers to people who've never written a line of code.
+
+---
+
+## VL-Code: The AI IDE
+
+**VL-Code** is the AI IDE for VisualLogic — a complete development environment where the LLM is not just an assistant, but the **core execution engine**.
+
+### Non-Linear Workflow Engine
+
+Unlike simple plan-then-execute tools, VL-Code uses **visual workflow DAGs** (Directed Acyclic Graphs). Each node can:
+
+- Call an LLM with targeted context
+- Spawn sub-agents for parallel tasks
+- Branch conditionally on results
+- Loop over collections (files, components, services)
+- Fork into parallel execution paths
+
+These workflows are fully visual. You see every node, every data flow, every decision point. They drive **every phase**: development, testing, debugging, and deployment.
+
+<p align="center">
+  <img src="https://raw.githubusercontent.com/VisualLogic-AI/VisualLogic.ai-VL/main/assets/images/vl-code-flow-editor.png" alt="VL-Code Flow Editor — Visual workflow DAG for multi-agent orchestration" width="90%"/>
+  <br/>
+  <em>Flow Editor — Visual workflow DAG with multi-agent orchestration</em>
+</p>
+
+<p align="center">
+  <img src="https://raw.githubusercontent.com/VisualLogic-AI/VisualLogic.ai-VL/main/assets/images/vl-code-meta-editor.png" alt="VL-Code Meta Editor — High-level architecture and agent pipeline view" width="90%"/>
+  <br/>
+  <em>Meta Editor — Architecture overview and agent pipeline visualization</em>
+</p>
+
+### Semantic Intelligence
+
+VL-Code doesn't just store files — it understands them:
+
+- **Real-time symbol index** — every declaration, reference, and variable tracked across the project
+- **Dependency graph** — automatic context loading based on file relationships
+- **Blueprint architecture** — living PRD, service map, and data model injected into every AI interaction
+- **Auto-fix engine** — syntax issues detected and repaired automatically
+
+### Developer Experience
+
+- Go-to-definition across all VL file types
+- Find all references for any symbol
+- Context-aware autocomplete
+- Impact analysis before renaming or removing
+- Session persistence across restarts
+- Multi-workspace project management
+
+---
+
+## From Development to Production
+
+VisualLogic applications aren't prototypes — they are production-ready:
+
+1. **Develop** — full IDE experience with AI generation and visual editing
+2. **Compile** — submit to the VL compiler, get instant preview URLs
+3. **Test** — automated browser-based verification via Playwright integration
+4. **Debug** — visual workflow tracing and impact analysis
+5. **Deploy** — publish to the VL Platform for production access
+
+---
+
+## Open Source Strategy
+
+| Layer | Status |
+|-------|--------|
+| VL Language Specification | Open |
+| VL Grammar & Parser | Open |
+| VL to Graph Mapping | Open |
+| AI Optimization Patches | Closed |
+| AI IDE (VL-Code) | Closed |
+| Graphic IDE | Closed |
+| Cloud Compiler | Closed |
+
+The VL language specification and core parsing infrastructure are open, enabling the community to build tools, integrations, and extensions around the VL ecosystem.
+
+---
+
+## Technical Stack
+
+- **Runtime**: Node.js + Express with SSE streaming
+- **Editor**: CodeMirror 5 (locally served, no CDN dependency)
+- **AI**: Anthropic Claude with prompt caching (47K-token VL spec)
+- **Architecture**: 4-segment prompt design for optimal token efficiency
+- **Integration**: MCP (Model Context Protocol) compatible, extensible skills system
+
+---
+
+## Who Is VisualLogic For?
+
+| Audience | Value |
+|----------|-------|
+| **AI-native developers** | Leverage deterministic AI generation for rapid full-stack development |
+| **Non-technical creators** | Build real applications through visual interfaces and natural language |
+| **Enterprise teams** | Standardize on a structured, auditable, AI-native development platform |
+| **Researchers** | Explore deterministic AI programming and language-graph reversibility |
+| **Platform builders** | Build on open VL specifications for custom tooling and integrations |
+
+---
+
+## Get Started
+
+- **Web Editor**: [editor.visuallogic.ai](https://editor.visuallogic.ai/)
+- **Community**: [Discord](https://discord.com/invite/KdaVtR7pzv)
+- **Tutorials**: [YouTube Playlist](https://www.youtube.com/playlist?list=PLJE6c8wBknRnCZIRv_VFa1dYswTSqoW21)
+
+---
+
+<p align="center">
+  <strong>The future of programming is deterministic AI + visual systems.</strong><br/>
+  <em>Build anything. Ship everything.</em>
+</p>

+ 58 - 0
ai.js

@@ -0,0 +1,58 @@
+/**
+ * ai.js — AI draft reply via Anthropic SDK
+ */
+const Anthropic = require('@anthropic-ai/sdk').default;
+const fs = require('fs');
+const path = require('path');
+
+let client = null;
+
+function initAI(apiKey) {
+  if (!apiKey) {
+    // Try .env file
+    try {
+      const envPath = path.join(__dirname, '.env');
+      const content = fs.readFileSync(envPath, 'utf-8');
+      for (const line of content.split('\n')) {
+        const m = line.match(/^ANTHROPIC_API_KEY=(.+)/);
+        if (m) { apiKey = m[1].trim(); break; }
+      }
+    } catch {}
+  }
+  if (!apiKey) return false;
+  client = new Anthropic({ apiKey });
+  return true;
+}
+
+function isReady() { return client !== null; }
+
+const SYSTEM = `You are a professional GitHub project maintainer for "GeneralNoCodeDP".
+Draft replies to GitHub issues and pull requests.
+- Be professional, helpful, concise
+- Reply in English
+- Bug reports: acknowledge, ask for repro steps if missing
+- Feature requests: thank reporter, explain consideration
+- PRs: review description, constructive feedback
+- Keep under 200 words unless complexity demands more
+- Use markdown where appropriate`;
+
+async function draftReply({ type, title, body, comments, repoName }) {
+  if (!client) throw new Error('AI not initialized');
+
+  const commentText = comments?.length
+    ? '\n### Comments:\n' + comments.map(c => `**${c.author}**: ${c.body}`).join('\n\n---\n')
+    : '';
+
+  const response = await client.messages.create({
+    model: 'claude-sonnet-4-5-20241022',
+    max_tokens: 1024,
+    system: SYSTEM,
+    messages: [{ role: 'user', content: `Repo: ${repoName}\n## ${type} — ${title}\n\n${body || '(empty)'}\n${commentText}\n\nDraft a reply:` }],
+  });
+
+  const draft = response.content[0].text;
+  const confidence = (comments?.length || 0) > 5 ? 'low' : (body?.length || 0) > 2000 ? 'medium' : 'high';
+  return { draft, confidence };
+}
+
+module.exports = { initAI, isReady, draftReply };

+ 299 - 0
extension.js

@@ -0,0 +1,299 @@
+const vscode = require('vscode');
+const path = require('path');
+const fs = require('fs');
+const gh = require('./gh');
+const ai = require('./ai');
+
+let panel = null;
+
+function activate(context) {
+  // Init AI
+  ai.initAI(process.env.ANTHROPIC_API_KEY);
+
+  // Main command: open dashboard
+  context.subscriptions.push(
+    vscode.commands.registerCommand('gh-hub.open', () => openDashboard(context))
+  );
+
+  // Quick push current workspace
+  context.subscriptions.push(
+    vscode.commands.registerCommand('gh-hub.pushCurrent', () => quickPush())
+  );
+
+  // Publish file as README
+  context.subscriptions.push(
+    vscode.commands.registerCommand('gh-hub.publishReadme', () => publishReadme())
+  );
+}
+
+// ═══════════════════════════════════════════════════════════════
+// Dashboard Panel
+// ═══════════════════════════════════════════════════════════════
+
+function openDashboard(context) {
+  if (panel) {
+    panel.reveal();
+    return;
+  }
+
+  panel = vscode.window.createWebviewPanel(
+    'ghHub', 'GH-Hub', vscode.ViewColumn.One,
+    { enableScripts: true, retainContextWhenHidden: true }
+  );
+
+  const htmlPath = path.join(context.extensionPath, 'media', 'dashboard.html');
+  panel.webview.html = fs.readFileSync(htmlPath, 'utf-8');
+
+  // Handle messages from webview
+  panel.webview.onDidReceiveMessage(async (msg) => {
+    try {
+      const result = await handleMessage(msg);
+      panel.webview.postMessage({ id: msg.id, command: msg.command, data: result });
+    } catch (e) {
+      panel.webview.postMessage({ id: msg.id, command: msg.command, error: e.message });
+    }
+  }, null, context.subscriptions);
+
+  panel.onDidDispose(() => { panel = null; });
+
+  // Start notification polling
+  startPolling();
+}
+
+async function handleMessage(msg) {
+  const { command, args } = msg;
+  switch (command) {
+    case 'status':
+      const status = await gh.checkStatus();
+      return { ...status, aiReady: ai.isReady() };
+
+    case 'listRepos':
+      return await gh.listRepos(args);
+
+    case 'createRepo':
+      return await gh.createRepo(args.name, args);
+
+    case 'listIssues':
+      return await gh.listIssues(args.repo, args);
+
+    case 'viewIssue':
+      return await gh.viewIssue(args.repo, args.number);
+
+    case 'commentIssue':
+      await gh.commentIssue(args.repo, args.number, args.body);
+      return { ok: true };
+
+    case 'closeIssue':
+      await gh.closeIssue(args.repo, args.number);
+      return { ok: true };
+
+    case 'reopenIssue':
+      await gh.reopenIssue(args.repo, args.number);
+      return { ok: true };
+
+    case 'listPRs':
+      return await gh.listPRs(args.repo, args);
+
+    case 'viewPR':
+      return await gh.viewPR(args.repo, args.number);
+
+    case 'commentPR':
+      await gh.commentPR(args.repo, args.number, args.body);
+      return { ok: true };
+
+    case 'mergePR':
+      await gh.mergePR(args.repo, args.number, args.method);
+      return { ok: true };
+
+    case 'closePR':
+      await gh.closePR(args.repo, args.number);
+      return { ok: true };
+
+    case 'listNotifications':
+      return await gh.listNotifications();
+
+    case 'markRead':
+      await gh.markNotificationRead(args.id);
+      return { ok: true };
+
+    case 'markAllRead':
+      await gh.markAllRead();
+      return { ok: true };
+
+    case 'gitStatus':
+      return await gh.gitStatus(args.dir);
+
+    case 'gitPush':
+      return await gh.gitPush(args.dir, args);
+
+    case 'aiDraft':
+      return await ai.draftReply(args);
+
+    case 'updateFile':
+      await gh.updateRepoFile(args.repo, args.path || 'README.md', args.content, args.message || 'Update file');
+      return { ok: true };
+
+    case 'uploadImage': {
+      // args: { repo, filePath (in repo), base64, message }
+      const imgArgs = ['api', '-X', 'PUT', `repos/${args.repo}/contents/${args.filePath}`,
+        '-f', `message=${args.message || 'Upload image'}`,
+        '-f', `content=${args.base64}`];
+      // Check if file exists to get SHA
+      try {
+        const { execFile } = require('child_process');
+        const { promisify } = require('util');
+        const run = promisify(execFile);
+        const existing = JSON.parse((await run('gh', ['api', `repos/${args.repo}/contents/${args.filePath}`])).stdout);
+        if (existing?.sha) imgArgs.push('-f', `sha=${existing.sha}`);
+      } catch {}
+      const { execFile: ef } = require('child_process');
+      const { promisify: p } = require('util');
+      const r = p(ef);
+      await r('gh', imgArgs);
+      return { ok: true, url: `https://raw.githubusercontent.com/${args.repo}/main/${args.filePath}` };
+    }
+
+    case 'pickImage': {
+      const imgFiles = await vscode.window.showOpenDialog({
+        canSelectFiles: true, canSelectFolders: false,
+        filters: { 'Images': ['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp'] }
+      });
+      if (imgFiles?.[0]) {
+        const imgBuf = fs.readFileSync(imgFiles[0].fsPath);
+        const base64 = imgBuf.toString('base64');
+        const name = path.basename(imgFiles[0].fsPath);
+        return { name, base64, size: imgBuf.length };
+      }
+      return null;
+    }
+
+    case 'fetchReadme': {
+      try {
+        const { execFile: ef2 } = require('child_process');
+        const { promisify: p2 } = require('util');
+        const run2 = p2(ef2);
+        const res = JSON.parse((await run2('gh', ['api', `repos/${args.repo}/contents/README.md`])).stdout);
+        return { content: Buffer.from(res.content, 'base64').toString('utf-8'), sha: res.sha };
+      } catch { return { content: '', sha: '' }; }
+    }
+
+    case 'pickFolder':
+      const uris = await vscode.window.showOpenDialog({ canSelectFolders: true, canSelectFiles: false });
+      return uris?.[0]?.fsPath || null;
+
+    case 'pickFile':
+      const files = await vscode.window.showOpenDialog({ canSelectFiles: true, canSelectFolders: false });
+      if (files?.[0]) {
+        const content = fs.readFileSync(files[0].fsPath, 'utf-8');
+        return { path: files[0].fsPath, name: path.basename(files[0].fsPath), content };
+      }
+      return null;
+
+    case 'getWorkspaceDir':
+      return vscode.workspace.workspaceFolders?.[0]?.uri.fsPath || null;
+
+    case 'quickSetup': {
+      const dir = args.dir;
+      // git init if needed
+      try { await gh.gitStatus(dir); }
+      catch {
+        await gh.gitInit(dir);
+      }
+      // Create repo if needed
+      if (args.createRepo) {
+        try { await gh.createRepo(args.repoName, { description: args.description, isPrivate: args.isPrivate }); }
+        catch (e) { if (!e.message.includes('already exists')) throw e; }
+        try { await gh.gitAddRemote(dir, 'origin', `https://github.com/${args.repoName}.git`); }
+        catch (e) { if (!e.message.includes('already exists')) throw e; }
+      }
+      // Add, commit, push
+      await gh.gitAddAll(dir);
+      try { await gh.gitCommit(dir, args.commitMessage || 'Initial commit'); }
+      catch (e) { if (!e.message.includes('nothing to commit')) throw e; }
+      const pushResult = await gh.gitPush(dir, { remote: 'origin' });
+      return { ok: true, message: pushResult };
+    }
+
+    default:
+      throw new Error(`Unknown command: ${command}`);
+  }
+}
+
+// ─── Notification Polling ────────────────────────────────────
+
+let pollInterval = null;
+
+function startPolling() {
+  if (pollInterval) return;
+  pollInterval = setInterval(async () => {
+    if (!panel) { clearInterval(pollInterval); pollInterval = null; return; }
+    try {
+      const notifs = await gh.listNotifications();
+      panel.webview.postMessage({ command: 'notifUpdate', data: notifs });
+    } catch {}
+  }, 60000);
+  // First poll immediately
+  (async () => {
+    try {
+      const notifs = await gh.listNotifications();
+      if (panel) panel.webview.postMessage({ command: 'notifUpdate', data: notifs });
+    } catch {}
+  })();
+}
+
+// ─── Quick Push ──────────────────────────────────────────────
+
+async function quickPush() {
+  const dir = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
+  if (!dir) { vscode.window.showErrorMessage('No workspace folder open'); return; }
+
+  try {
+    const status = await gh.gitStatus(dir);
+    const result = await gh.gitPush(dir, {});
+    vscode.window.showInformationMessage(`Pushed ${status.branch} to origin`);
+  } catch (e) {
+    vscode.window.showErrorMessage(`Push failed: ${e.message}`);
+  }
+}
+
+// ─── Publish README ──────────────────────────────────────────
+
+async function publishReadme() {
+  const editor = vscode.window.activeTextEditor;
+  if (!editor) { vscode.window.showErrorMessage('No file open'); return; }
+
+  const content = editor.document.getText();
+  const fileName = path.basename(editor.document.fileName);
+
+  // Ask which repo
+  const status = await gh.checkStatus();
+  if (!status.ok) { vscode.window.showErrorMessage('Not authenticated. Run: gh auth login'); return; }
+
+  const repos = await gh.listRepos({ limit: 30 });
+  if (!repos?.length) { vscode.window.showErrorMessage('No repos found'); return; }
+
+  const pick = await vscode.window.showQuickPick(
+    repos.map(r => ({ label: `${r.owner.login}/${r.name}`, description: r.description || '' })),
+    { placeHolder: 'Select target repo for README' }
+  );
+  if (!pick) return;
+
+  const msg = await vscode.window.showInputBox({
+    prompt: 'Commit message',
+    value: `Update README from ${fileName}`
+  });
+  if (!msg) return;
+
+  try {
+    await gh.updateRepoFile(pick.label, 'README.md', content, msg);
+    vscode.window.showInformationMessage(`README updated on ${pick.label}`);
+  } catch (e) {
+    vscode.window.showErrorMessage(`Failed: ${e.message}`);
+  }
+}
+
+function deactivate() {
+  if (pollInterval) clearInterval(pollInterval);
+}
+
+module.exports = { activate, deactivate };

+ 215 - 0
gh.js

@@ -0,0 +1,215 @@
+/**
+ * gh.js — GitHub CLI wrapper for VS Code extension
+ * All GitHub operations via `gh` CLI for proper auth.
+ */
+const { execFile } = require('child_process');
+const { promisify } = require('util');
+
+const run = promisify(execFile);
+
+async function gh(args, opts = {}) {
+  try {
+    const { stdout } = await run('gh', args, { timeout: 30000, maxBuffer: 10 * 1024 * 1024, ...opts });
+    return stdout.trim();
+  } catch (e) {
+    throw new Error(e.stderr?.trim() || e.message);
+  }
+}
+
+async function ghJSON(args, opts) {
+  const raw = await gh(args, opts);
+  if (!raw) return null;
+  return JSON.parse(raw);
+}
+
+// ─── Auth ────────────────────────────────────────────────────
+
+async function checkStatus() {
+  try {
+    const out = await gh(['auth', 'status']);
+    const user = out.match(/Logged in to github\.com account (\S+)/)?.[1]
+      || out.match(/account (\S+)/)?.[1] || 'unknown';
+    return { ok: true, user, raw: out };
+  } catch (e) {
+    return { ok: false, error: e.message };
+  }
+}
+
+// ─── Repos ───────────────────────────────────────────────────
+
+async function listRepos({ limit = 30 } = {}) {
+  return ghJSON([
+    'repo', 'list', '--json',
+    'name,owner,description,url,stargazerCount,forkCount,primaryLanguage,updatedAt,isPrivate',
+    '--limit', String(limit)
+  ]);
+}
+
+async function createRepo(name, { description = '', isPrivate = false } = {}) {
+  const args = ['repo', 'create', name];
+  if (description) args.push('--description', description);
+  args.push(isPrivate ? '--private' : '--public');
+  return gh(args);
+}
+
+// ─── Issues ──────────────────────────────────────────────────
+
+async function listIssues(nwo, { state = 'open', limit = 30 } = {}) {
+  return ghJSON([
+    'issue', 'list', '-R', nwo, '--json',
+    'number,title,author,labels,state,createdAt,updatedAt,comments,url,body',
+    '--limit', String(limit), '--state', state
+  ]);
+}
+
+async function viewIssue(nwo, number) {
+  const issue = await ghJSON([
+    'issue', 'view', String(number), '-R', nwo, '--json',
+    'number,title,author,labels,state,createdAt,updatedAt,body,url,comments'
+  ]);
+  try {
+    const comments = await ghJSON([
+      'api', `repos/${nwo}/issues/${number}/comments`,
+      '--jq', '[.[] | {id:.id, author:.user.login, body:.body, createdAt:.created_at}]'
+    ]);
+    issue.commentList = comments || [];
+  } catch { issue.commentList = []; }
+  return issue;
+}
+
+async function commentIssue(nwo, number, body) {
+  return gh(['issue', 'comment', String(number), '-R', nwo, '--body', body]);
+}
+
+async function closeIssue(nwo, number) {
+  return gh(['issue', 'close', String(number), '-R', nwo]);
+}
+
+async function reopenIssue(nwo, number) {
+  return gh(['issue', 'reopen', String(number), '-R', nwo]);
+}
+
+// ─── Pull Requests ───────────────────────────────────────────
+
+async function listPRs(nwo, { state = 'open', limit = 30 } = {}) {
+  return ghJSON([
+    'pr', 'list', '-R', nwo, '--json',
+    'number,title,author,state,createdAt,updatedAt,url,body,headRefName,baseRefName,isDraft,comments',
+    '--limit', String(limit), '--state', state
+  ]);
+}
+
+async function viewPR(nwo, number) {
+  const pr = await ghJSON([
+    'pr', 'view', String(number), '-R', nwo, '--json',
+    'number,title,author,state,createdAt,updatedAt,body,url,headRefName,baseRefName,isDraft,comments,additions,deletions,changedFiles'
+  ]);
+  try {
+    const comments = await ghJSON([
+      'api', `repos/${nwo}/issues/${number}/comments`,
+      '--jq', '[.[] | {id:.id, author:.user.login, body:.body, createdAt:.created_at}]'
+    ]);
+    pr.commentList = comments || [];
+  } catch { pr.commentList = []; }
+  return pr;
+}
+
+async function commentPR(nwo, number, body) {
+  return gh(['pr', 'comment', String(number), '-R', nwo, '--body', body]);
+}
+
+async function mergePR(nwo, number, method = 'merge') {
+  return gh(['pr', 'merge', String(number), '-R', nwo, `--${method}`, '--yes']);
+}
+
+async function closePR(nwo, number) {
+  return gh(['pr', 'close', String(number), '-R', nwo]);
+}
+
+// ─── Notifications ───────────────────────────────────────────
+
+async function listNotifications() {
+  try {
+    const raw = await gh(['api', '/notifications']);
+    if (!raw) return [];
+    return JSON.parse(raw).map(n => ({
+      id: n.id, reason: n.reason, unread: n.unread,
+      updatedAt: n.updated_at, title: n.subject?.title,
+      type: n.subject?.type, url: n.subject?.url,
+      repo: n.repository?.full_name
+    }));
+  } catch { return []; }
+}
+
+async function markNotificationRead(threadId) {
+  return gh(['api', '-X', 'PATCH', `/notifications/threads/${threadId}`]);
+}
+
+async function markAllRead() {
+  return gh(['api', '-X', 'PUT', '/notifications', '-f', `last_read_at=${new Date().toISOString()}`]);
+}
+
+// ─── Git Operations ──────────────────────────────────────────
+
+async function gitStatus(dir) {
+  const branch = (await run('git', ['branch', '--show-current'], { cwd: dir })).stdout.trim();
+  const status = (await run('git', ['status', '--short'], { cwd: dir })).stdout.trim();
+  const remotes = (await run('git', ['remote', '-v'], { cwd: dir })).stdout.trim();
+  let ahead = 0, behind = 0;
+  try {
+    const ab = (await run('git', ['rev-list', '--left-right', '--count', `origin/${branch}...HEAD`], { cwd: dir })).stdout.trim();
+    const p = ab.split(/\s+/);
+    behind = parseInt(p[0]) || 0;
+    ahead = parseInt(p[1]) || 0;
+  } catch {}
+  return { branch, status, remotes, ahead, behind };
+}
+
+async function gitPush(dir, { remote = 'origin', branch = '' } = {}) {
+  if (!branch) branch = (await run('git', ['branch', '--show-current'], { cwd: dir })).stdout.trim();
+  const result = await run('git', ['push', '-u', remote, branch], { cwd: dir, timeout: 60000 });
+  return result.stdout || result.stderr || 'Push completed.';
+}
+
+async function gitInit(dir) {
+  return (await run('git', ['init'], { cwd: dir })).stdout.trim();
+}
+
+async function gitAddRemote(dir, name, url) {
+  return (await run('git', ['remote', 'add', name, url], { cwd: dir })).stdout.trim();
+}
+
+async function gitAddAll(dir) {
+  return (await run('git', ['add', '-A'], { cwd: dir })).stdout.trim();
+}
+
+async function gitCommit(dir, message) {
+  return (await run('git', ['commit', '-m', message], { cwd: dir })).stdout.trim();
+}
+
+// ─── README / File Push ──────────────────────────────────────
+
+async function updateRepoFile(nwo, filePath, content, message) {
+  // Get current file SHA if exists
+  let sha = '';
+  try {
+    const existing = await ghJSON(['api', `repos/${nwo}/contents/${filePath}`]);
+    sha = existing?.sha || '';
+  } catch {}
+
+  const base64 = Buffer.from(content).toString('base64');
+  const args = ['api', '-X', 'PUT', `repos/${nwo}/contents/${filePath}`,
+    '-f', `message=${message}`,
+    '-f', `content=${base64}`];
+  if (sha) args.push('-f', `sha=${sha}`);
+  return gh(args);
+}
+
+module.exports = {
+  checkStatus, listRepos, createRepo,
+  listIssues, viewIssue, commentIssue, closeIssue, reopenIssue,
+  listPRs, viewPR, commentPR, mergePR, closePR,
+  listNotifications, markNotificationRead, markAllRead,
+  gitStatus, gitPush, gitInit, gitAddRemote, gitAddAll, gitCommit,
+  updateRepoFile
+};

+ 913 - 0
media/dashboard.html

@@ -0,0 +1,913 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>GH-Hub</title>
+<style>
+*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
+:root {
+  --bg: var(--vscode-editor-background, #1e1e1e);
+  --bg2: var(--vscode-sideBar-background, #252526);
+  --bg3: var(--vscode-input-background, #3c3c3c);
+  --border: var(--vscode-panel-border, #474747);
+  --text: var(--vscode-editor-foreground, #cccccc);
+  --text2: var(--vscode-descriptionForeground, #858585);
+  --accent: var(--vscode-textLink-foreground, #3794ff);
+  --green: #3fb950; --yellow: #d29922; --red: #f85149; --purple: #a371f7;
+  --font: var(--vscode-font-family, system-ui);
+  --mono: var(--vscode-editor-font-family, monospace);
+  --radius: 4px;
+}
+body { font-family: var(--font); background: var(--bg); color: var(--text); font-size: 13px; line-height: 1.5; height: 100vh; display: flex; flex-direction: column; }
+a { color: var(--accent); text-decoration: none; }
+a:hover { text-decoration: underline; }
+
+button {
+  font-family: var(--font); font-size: 12px; padding: 4px 12px;
+  border: 1px solid var(--border); border-radius: var(--radius);
+  background: var(--bg3); color: var(--text); cursor: pointer;
+}
+button:hover { opacity: 0.85; }
+button.primary { background: #238636; border-color: #2ea043; color: #fff; }
+button.danger { background: #da3633; border-color: #f85149; color: #fff; }
+button.ai { background: #1f1a3e; border-color: var(--purple); color: var(--purple); }
+button:disabled { opacity: 0.4; cursor: default; }
+
+input, textarea, select {
+  font-family: var(--font); font-size: 12px; padding: 4px 8px;
+  border: 1px solid var(--border); border-radius: var(--radius);
+  background: var(--bg3); color: var(--text); width: 100%;
+}
+textarea { resize: vertical; min-height: 80px; font-family: var(--mono); font-size: 12px; }
+
+/* ─── Header ──────────────────────────────── */
+.header {
+  display: flex; align-items: center; gap: 10px; padding: 8px 14px;
+  background: var(--bg2); border-bottom: 1px solid var(--border);
+}
+.header h1 { font-size: 15px; font-weight: 600; }
+.header h1 b { color: var(--accent); }
+.header .user { color: var(--text2); font-size: 12px; }
+.header .spacer { flex: 1; }
+.badge {
+  background: var(--accent); color: #000; font-size: 10px; font-weight: 700;
+  padding: 1px 7px; border-radius: 8px; cursor: pointer; min-width: 18px; text-align: center;
+}
+.badge.zero { background: var(--bg3); color: var(--text2); }
+.dot { width: 7px; height: 7px; border-radius: 50%; display: inline-block; }
+.dot.ok { background: var(--green); }
+.dot.err { background: var(--red); }
+
+/* ─── Tabs ────────────────────────────────── */
+.tabs {
+  display: flex; background: var(--bg2); border-bottom: 1px solid var(--border); padding: 0 14px;
+}
+.tab {
+  padding: 7px 14px; font-size: 12px; color: var(--text2); cursor: pointer;
+  border-bottom: 2px solid transparent;
+}
+.tab:hover { color: var(--text); }
+.tab.active { color: var(--text); border-bottom-color: var(--accent); }
+
+/* ─── Panels ──────────────────────────────── */
+.content { flex: 1; overflow: hidden; }
+.panel { display: none; height: 100%; overflow-y: auto; padding: 14px; }
+.panel.active { display: flex; flex-direction: column; }
+
+/* ─── Repos ───────────────────────────────── */
+.toolbar { display: flex; gap: 8px; align-items: center; margin-bottom: 10px; }
+.toolbar h2 { font-size: 14px; flex: 1; }
+.repo-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 8px; flex: 1; overflow-y: auto; }
+.repo-card {
+  background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius);
+  padding: 12px; cursor: pointer;
+}
+.repo-card:hover { border-color: var(--accent); }
+.repo-card.sel { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }
+.repo-card h3 { font-size: 13px; color: var(--accent); }
+.repo-card .desc { color: var(--text2); font-size: 11px; margin: 2px 0 6px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.repo-meta { display: flex; gap: 12px; font-size: 11px; color: var(--text2); }
+.lang-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; margin-right: 2px; vertical-align: middle; }
+.tag-private { color: var(--yellow); border: 1px solid var(--yellow); padding: 0 4px; border-radius: 3px; font-size: 9px; }
+
+/* ─── Split View ──────────────────────────── */
+.split { display: flex; height: 100%; gap: 0; flex: 1; overflow: hidden; }
+.list-pane { width: 320px; min-width: 240px; border-right: 1px solid var(--border); overflow-y: auto; display: flex; flex-direction: column; }
+.detail-pane { flex: 1; overflow-y: auto; padding: 14px; }
+
+.filter-bar { padding: 6px 10px; border-bottom: 1px solid var(--border); display: flex; gap: 6px; align-items: center; }
+.filter-bar .label { font-size: 11px; color: var(--accent); font-weight: 600; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
+.filter-bar select { width: auto; font-size: 11px; }
+
+.item-card { padding: 8px 12px; border-bottom: 1px solid var(--border); cursor: pointer; }
+.item-card:hover { background: var(--bg3); }
+.item-card.sel { background: var(--bg3); border-left: 3px solid var(--accent); }
+.item-card .num { color: var(--text2); font-size: 11px; }
+.item-card .title { font-size: 12px; font-weight: 500; }
+.item-card .meta { font-size: 10px; color: var(--text2); display: flex; gap: 8px; margin-top: 2px; }
+.label-tag { display: inline-block; font-size: 9px; font-weight: 600; padding: 0 5px; border-radius: 8px; margin-right: 3px; }
+
+/* ─── Detail ──────────────────────────────── */
+.detail-header h2 { font-size: 15px; margin-bottom: 2px; }
+.detail-header .meta { font-size: 11px; color: var(--text2); margin-bottom: 10px; }
+.detail-body {
+  background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius);
+  padding: 12px; margin-bottom: 12px; font-size: 12px; line-height: 1.6; word-wrap: break-word;
+}
+.detail-body pre { background: var(--bg); padding: 8px; border-radius: 3px; overflow-x: auto; font-family: var(--mono); font-size: 11px; margin: 6px 0; }
+.detail-body code { font-family: var(--mono); font-size: 11px; background: var(--bg3); padding: 1px 3px; border-radius: 2px; }
+.detail-body pre code { background: none; padding: 0; }
+
+.comment { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); padding: 10px 12px; margin-bottom: 6px; }
+.comment .author { font-weight: 600; font-size: 12px; }
+.comment .time { color: var(--text2); font-size: 10px; margin-left: 6px; }
+.comment .body { margin-top: 6px; font-size: 12px; line-height: 1.5; }
+
+.reply-area { margin-top: 12px; }
+.reply-actions { display: flex; gap: 6px; margin-top: 6px; align-items: center; }
+.confidence { font-size: 10px; padding: 1px 6px; border-radius: 8px; font-weight: 600; }
+.confidence.high { background: #0d3321; color: var(--green); }
+.confidence.medium { background: #3d2e00; color: var(--yellow); }
+.confidence.low { background: #3d1214; color: var(--red); }
+
+.empty { display: flex; align-items: center; justify-content: center; height: 100%; color: var(--text2); flex-direction: column; gap: 6px; padding: 30px; text-align: center; }
+
+/* ─── Notifications ───────────────────────── */
+.notif-item {
+  display: flex; gap: 10px; padding: 8px 12px; background: var(--bg2);
+  border: 1px solid var(--border); border-radius: var(--radius); margin-bottom: 6px; align-items: flex-start;
+}
+.notif-item .icon { font-size: 14px; margin-top: 2px; }
+.notif-item .info { flex: 1; }
+.notif-item .repo { font-size: 10px; color: var(--text2); }
+.notif-item .title { font-size: 12px; }
+.notif-item .reason { font-size: 10px; color: var(--text2); }
+
+/* ─── Push ────────────────────────────────── */
+.push-panel { max-width: 600px; }
+.field { margin-bottom: 10px; }
+.field label { font-size: 11px; color: var(--text2); margin-bottom: 3px; display: block; }
+.field-row { display: flex; gap: 6px; }
+.git-info { margin: 10px 0; }
+.git-info .branch { font-size: 13px; font-weight: 600; color: var(--green); }
+.git-info .counts { font-size: 11px; color: var(--text2); margin-top: 2px; }
+.git-info pre { background: var(--bg2); padding: 8px; border-radius: var(--radius); font-family: var(--mono); font-size: 11px; margin-top: 6px; max-height: 150px; overflow-y: auto; }
+.log { background: var(--bg2); border: 1px solid var(--border); border-radius: var(--radius); padding: 8px; font-family: var(--mono); font-size: 11px; min-height: 60px; white-space: pre-wrap; color: var(--text2); margin-top: 10px; }
+.log .ok { color: var(--green); }
+.log .err { color: var(--red); }
+
+/* ─── Publish Tab ─────────────────────────── */
+.pub-top { display: flex; gap: 8px; align-items: center; margin-bottom: 8px; flex-shrink: 0; }
+.pub-top h2 { font-size: 14px; flex: 1; }
+.pub-settings { display: flex; gap: 6px; align-items: center; flex-shrink: 0; margin-bottom: 8px; }
+.pub-settings select, .pub-settings input { width: auto; font-size: 11px; }
+.pub-settings input { max-width: 180px; }
+.pub-split { display: flex; flex: 1; gap: 0; overflow: hidden; border: 1px solid var(--border); border-radius: var(--radius); }
+.pub-editor { flex: 1; display: flex; flex-direction: column; border-right: 1px solid var(--border); }
+.pub-toolbar { display: flex; gap: 2px; padding: 4px 6px; background: var(--bg2); border-bottom: 1px solid var(--border); flex-wrap: wrap; }
+.pub-toolbar button { font-size: 11px; padding: 2px 7px; min-width: 26px; border: none; background: transparent; color: var(--text2); }
+.pub-toolbar button:hover { background: var(--bg3); color: var(--text); border-radius: 3px; }
+.pub-toolbar .sep { width: 1px; background: var(--border); margin: 2px 4px; }
+.pub-editor textarea { flex: 1; border: none; border-radius: 0; resize: none; padding: 10px; font-family: var(--mono); font-size: 12px; line-height: 1.6; background: var(--bg); }
+.pub-preview { flex: 1; overflow-y: auto; padding: 16px; background: var(--bg); font-size: 13px; line-height: 1.7; }
+.pub-preview h1 { font-size: 22px; border-bottom: 1px solid var(--border); padding-bottom: 6px; margin: 16px 0 10px; }
+.pub-preview h2 { font-size: 18px; border-bottom: 1px solid var(--border); padding-bottom: 4px; margin: 14px 0 8px; }
+.pub-preview h3 { font-size: 15px; margin: 12px 0 6px; }
+.pub-preview h4 { font-size: 13px; margin: 10px 0 4px; }
+.pub-preview p { margin: 6px 0; }
+.pub-preview img { max-width: 100%; border-radius: 4px; margin: 8px 0; }
+.pub-preview pre { background: var(--bg2); padding: 10px; border-radius: 4px; overflow-x: auto; font-family: var(--mono); font-size: 11px; margin: 8px 0; }
+.pub-preview code { font-family: var(--mono); font-size: 11px; background: var(--bg3); padding: 1px 4px; border-radius: 2px; }
+.pub-preview pre code { background: none; padding: 0; }
+.pub-preview blockquote { border-left: 3px solid var(--accent); padding: 4px 12px; margin: 8px 0; color: var(--text2); }
+.pub-preview table { border-collapse: collapse; margin: 8px 0; width: 100%; }
+.pub-preview th, .pub-preview td { border: 1px solid var(--border); padding: 4px 8px; font-size: 12px; text-align: left; }
+.pub-preview th { background: var(--bg2); }
+.pub-preview ul, .pub-preview ol { padding-left: 20px; margin: 6px 0; }
+.pub-preview li { margin: 2px 0; }
+.pub-preview hr { border: none; border-top: 1px solid var(--border); margin: 12px 0; }
+.pub-preview a { color: var(--accent); }
+.img-upload-area { padding: 6px 10px; background: var(--bg2); border-top: 1px solid var(--border); font-size: 11px; color: var(--text2); display: flex; align-items: center; gap: 6px; }
+.img-upload-area button { font-size: 11px; padding: 2px 8px; }
+
+/* ─── Modal ───────────────────────────────── */
+.modal-bg { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: none; align-items: center; justify-content: center; z-index: 99; }
+.modal-bg.show { display: flex; }
+.modal { background: var(--bg2); border: 1px solid var(--border); border-radius: 6px; padding: 18px; width: 380px; }
+.modal h3 { margin-bottom: 12px; font-size: 14px; }
+.modal .actions { display: flex; gap: 6px; justify-content: flex-end; margin-top: 14px; }
+
+.spinner { display: inline-block; width: 12px; height: 12px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin .5s linear infinite; }
+@keyframes spin { to { transform: rotate(360deg); } }
+.loading { display: flex; align-items: center; gap: 6px; padding: 20px; color: var(--text2); }
+</style>
+</head>
+<body>
+
+<div class="header">
+  <h1>GH-<b>Hub</b></h1>
+  <span class="dot" id="hDot"></span>
+  <span class="user" id="hUser">...</span>
+  <span class="spacer"></span>
+  <span class="badge zero" id="hBadge" onclick="switchTab('notifications')">0</span>
+</div>
+
+<div class="tabs">
+  <div class="tab active" data-t="repos" onclick="switchTab('repos')">Repos</div>
+  <div class="tab" data-t="issues" onclick="switchTab('issues')">Issues</div>
+  <div class="tab" data-t="pulls" onclick="switchTab('pulls')">PRs</div>
+  <div class="tab" data-t="notifications" onclick="switchTab('notifications')">Notifications</div>
+  <div class="tab" data-t="push" onclick="switchTab('push')">Push</div>
+  <div class="tab" data-t="publish" onclick="switchTab('publish')">Publish</div>
+</div>
+
+<div class="content">
+  <!-- ═══ Repos ═══ -->
+  <div class="panel active" id="p-repos">
+    <div class="toolbar">
+      <h2>Repositories</h2>
+      <button onclick="loadRepos()">Refresh</button>
+      <button class="primary" onclick="showModal('createRepoModal')">+ New</button>
+    </div>
+    <div class="repo-grid" id="repoGrid"><div class="loading"><span class="spinner"></span> Loading...</div></div>
+  </div>
+
+  <!-- ═══ Issues ═══ -->
+  <div class="panel" id="p-issues" style="padding:0;">
+    <div class="split">
+      <div class="list-pane">
+        <div class="filter-bar">
+          <span class="label" id="iRepoLabel">Select a repo</span>
+          <select id="iState" onchange="loadIssues()"><option value="open">Open</option><option value="closed">Closed</option><option value="all">All</option></select>
+        </div>
+        <div id="iList" style="flex:1;overflow-y:auto;"><div class="empty">Click a repo first</div></div>
+      </div>
+      <div class="detail-pane" id="iDetail"><div class="empty">Select an issue</div></div>
+    </div>
+  </div>
+
+  <!-- ═══ PRs ═══ -->
+  <div class="panel" id="p-pulls" style="padding:0;">
+    <div class="split">
+      <div class="list-pane">
+        <div class="filter-bar">
+          <span class="label" id="pRepoLabel">Select a repo</span>
+          <select id="pState" onchange="loadPRs()"><option value="open">Open</option><option value="closed">Closed</option><option value="all">All</option></select>
+        </div>
+        <div id="pList" style="flex:1;overflow-y:auto;"><div class="empty">Click a repo first</div></div>
+      </div>
+      <div class="detail-pane" id="pDetail"><div class="empty">Select a PR</div></div>
+    </div>
+  </div>
+
+  <!-- ═══ Notifications ═══ -->
+  <div class="panel" id="p-notifications">
+    <div class="toolbar">
+      <h2>Notifications</h2>
+      <button onclick="loadNotifs()">Refresh</button>
+      <button onclick="markAllRead()">Mark All Read</button>
+    </div>
+    <div id="notifList" style="flex:1;overflow-y:auto;max-width:650px;"><div class="loading"><span class="spinner"></span> Loading...</div></div>
+  </div>
+
+  <!-- ═══ Push ═══ -->
+  <div class="panel" id="p-push">
+    <h2 style="font-size:14px;margin-bottom:10px;">Quick Push</h2>
+    <div class="push-panel">
+      <div class="field">
+        <label>Local Directory</label>
+        <div class="field-row">
+          <input id="pushDir" placeholder="/path/to/project">
+          <button onclick="pickFolder()" style="white-space:nowrap;">Browse</button>
+          <button onclick="useWorkspace()" style="white-space:nowrap;">Workspace</button>
+        </div>
+      </div>
+      <button onclick="checkGit()">Check Status</button>
+      <div class="git-info" id="gitInfo"></div>
+      <div style="display:flex;gap:6px;margin-top:8px;">
+        <button class="primary" id="pushBtn" onclick="doPush()" disabled>Push</button>
+        <button onclick="doQuickSetup()">Quick Setup + Push</button>
+      </div>
+      <div class="log" id="pushLog" style="display:none;"></div>
+    </div>
+  </div>
+
+  <!-- ═══ Publish ═══ -->
+  <div class="panel" id="p-publish">
+    <div class="pub-top">
+      <h2>Publish to GitHub</h2>
+      <span id="pubResult" style="font-size:12px;"></span>
+    </div>
+    <div class="pub-settings">
+      <select id="pubRepo" onchange="onRepoChange()"><option value="">Target Repo</option></select>
+      <input id="pubPath" value="README.md" style="max-width:140px;" placeholder="Path (e.g. README.md)">
+      <input id="pubMsg" value="Update README" style="max-width:200px;" placeholder="Commit message">
+      <button onclick="fetchExistingReadme()">Load Existing</button>
+      <button class="primary" onclick="doPublish()" id="pubBtn">Publish</button>
+    </div>
+    <div class="pub-split">
+      <div class="pub-editor">
+        <div class="pub-toolbar">
+          <button onclick="tbWrap('**','**')" title="Bold"><b>B</b></button>
+          <button onclick="tbWrap('*','*')" title="Italic"><i>I</i></button>
+          <button onclick="tbWrap('~~','~~')" title="Strikethrough"><s>S</s></button>
+          <div class="sep"></div>
+          <button onclick="tbPrefix('# ')" title="H1">H1</button>
+          <button onclick="tbPrefix('## ')" title="H2">H2</button>
+          <button onclick="tbPrefix('### ')" title="H3">H3</button>
+          <div class="sep"></div>
+          <button onclick="tbPrefix('- ')" title="Bullet list">&#8226;</button>
+          <button onclick="tbPrefix('1. ')" title="Numbered list">1.</button>
+          <button onclick="tbPrefix('> ')" title="Blockquote">&gt;</button>
+          <div class="sep"></div>
+          <button onclick="tbLink()" title="Insert link">Link</button>
+          <button onclick="tbImage()" title="Insert image from URL">Img</button>
+          <button onclick="tbUploadImage()" title="Upload image to repo">Upload</button>
+          <div class="sep"></div>
+          <button onclick="tbWrap('`','`')" title="Inline code">code</button>
+          <button onclick="tbCodeBlock()" title="Code block">```</button>
+          <button onclick="tbPrefix('---')" title="Horizontal rule">HR</button>
+          <div class="sep"></div>
+          <button onclick="tbTable()" title="Insert table">Table</button>
+        </div>
+        <textarea id="pubEditor" placeholder="Write your markdown here..." oninput="updatePreview()"></textarea>
+        <div class="img-upload-area">
+          <span>Images:</span>
+          <button onclick="tbUploadImage()">Browse & Upload</button>
+          <span id="imgStatus" style="color:var(--text2);font-size:10px;"></span>
+        </div>
+      </div>
+      <div class="pub-preview" id="pubPreview">
+        <div class="empty" style="height:auto;">Live preview will appear here</div>
+      </div>
+    </div>
+  </div>
+</div>
+
+<!-- ═══ Create Repo Modal ═══ -->
+<div class="modal-bg" id="createRepoModal">
+  <div class="modal">
+    <h3>New Repository</h3>
+    <div class="field"><label>Name</label><input id="nrName" placeholder="my-project"></div>
+    <div class="field"><label>Description</label><input id="nrDesc" placeholder="optional"></div>
+    <div class="field"><label><input type="checkbox" id="nrPrivate" style="width:auto;margin-right:4px;">Private</label></div>
+    <div class="actions">
+      <button onclick="hideModal('createRepoModal')">Cancel</button>
+      <button class="primary" onclick="createRepo()">Create</button>
+    </div>
+  </div>
+</div>
+
+<!-- ═══ Quick Setup Modal ═══ -->
+<div class="modal-bg" id="quickSetupModal">
+  <div class="modal">
+    <h3>Quick Setup + Push</h3>
+    <p style="font-size:11px;color:var(--text2);margin-bottom:10px;">Create repo, init git, commit all, and push in one step.</p>
+    <div class="field"><label>Repo Name (owner/name)</label><input id="qsName" placeholder="GeneralNoCodeDP/my-project"></div>
+    <div class="field"><label>Description</label><input id="qsDesc"></div>
+    <div class="field"><label>Commit Message</label><input id="qsMsg" value="Initial commit"></div>
+    <div class="field"><label><input type="checkbox" id="qsPrivate" style="width:auto;margin-right:4px;">Private</label></div>
+    <div class="actions">
+      <button onclick="hideModal('quickSetupModal')">Cancel</button>
+      <button class="primary" onclick="execQuickSetup()">Go</button>
+    </div>
+  </div>
+</div>
+
+<script>
+const vscode = acquireVsCodeApi();
+let S = { repo: null, repos: [], aiReady: false, pubContent: '', pendingCallbacks: {} };
+let msgId = 0;
+
+// ─── Bridge ──────────────────────────────────
+
+function send(command, args = {}) {
+  return new Promise((resolve, reject) => {
+    const id = ++msgId;
+    S.pendingCallbacks[id] = { resolve, reject };
+    vscode.postMessage({ id, command, args });
+  });
+}
+
+window.addEventListener('message', e => {
+  const msg = e.data;
+  // Async response
+  if (msg.id && S.pendingCallbacks[msg.id]) {
+    const cb = S.pendingCallbacks[msg.id];
+    delete S.pendingCallbacks[msg.id];
+    if (msg.error) cb.reject(new Error(msg.error));
+    else cb.resolve(msg.data);
+    return;
+  }
+  // Push events
+  if (msg.command === 'notifUpdate') {
+    const n = Array.isArray(msg.data) ? msg.data.length : 0;
+    setBadge(n);
+  }
+});
+
+// ─── Tabs ────────────────────────────────────
+
+function switchTab(t) {
+  document.querySelectorAll('.tab').forEach(el => el.classList.toggle('active', el.dataset.t === t));
+  document.querySelectorAll('.panel').forEach(el => el.classList.toggle('active', el.id === `p-${t}`));
+  if (t === 'notifications') loadNotifs();
+  if (t === 'publish') populateRepoSelect();
+}
+
+// ─── Init ────────────────────────────────────
+
+async function init() {
+  try {
+    const s = await send('status');
+    document.getElementById('hDot').className = s.ok ? 'dot ok' : 'dot err';
+    document.getElementById('hUser').textContent = s.ok ? s.user : 'Not authenticated';
+    S.aiReady = s.aiReady;
+  } catch (e) {
+    document.getElementById('hDot').className = 'dot err';
+    document.getElementById('hUser').textContent = 'Error';
+  }
+  loadRepos();
+}
+
+function setBadge(n) {
+  const b = document.getElementById('hBadge');
+  b.textContent = n;
+  b.className = n > 0 ? 'badge' : 'badge zero';
+}
+
+function showModal(id) { document.getElementById(id).classList.add('show'); }
+function hideModal(id) { document.getElementById(id).classList.remove('show'); }
+
+// ─── Repos ───────────────────────────────────
+
+const LC = { JavaScript:'#f1e05a', TypeScript:'#3178c6', Python:'#3572a5', Go:'#00add8', Rust:'#dea584', HTML:'#e34c26', CSS:'#563d7c', Shell:'#89e051', Vue:'#41b883' };
+
+async function loadRepos() {
+  const g = document.getElementById('repoGrid');
+  g.innerHTML = '<div class="loading"><span class="spinner"></span> Loading...</div>';
+  try {
+    const repos = await send('listRepos');
+    S.repos = repos || [];
+    if (!repos?.length) { g.innerHTML = '<div class="empty">No repos</div>'; return; }
+    g.innerHTML = repos.map(r => {
+      const nwo = r.owner.login + '/' + r.name;
+      const l = r.primaryLanguage?.name || '';
+      const c = LC[l] || '#858585';
+      return `<div class="repo-card${S.repo===nwo?' sel':''}" onclick="selectRepo('${nwo}')" data-r="${nwo}">
+        <h3>${r.name}</h3>
+        <div class="desc">${esc(r.description||'No description')}</div>
+        <div class="repo-meta">
+          ${l?`<span><span class="lang-dot" style="background:${c}"></span>${l}</span>`:''}
+          <span>&#9733;${r.stargazerCount}</span>
+          <span>${timeAgo(r.updatedAt)}</span>
+          ${r.isPrivate?'<span class="tag-private">Private</span>':''}
+        </div></div>`;
+    }).join('');
+  } catch (e) { g.innerHTML = `<div class="empty">Error: ${esc(e.message)}</div>`; }
+}
+
+function selectRepo(nwo) {
+  S.repo = nwo;
+  document.querySelectorAll('.repo-card').forEach(c => c.classList.toggle('sel', c.dataset.r === nwo));
+  document.getElementById('iRepoLabel').textContent = nwo;
+  document.getElementById('pRepoLabel').textContent = nwo;
+  loadIssues(); loadPRs();
+}
+
+async function createRepo() {
+  const name = document.getElementById('nrName').value.trim();
+  if (!name) return;
+  try {
+    await send('createRepo', { name, description: document.getElementById('nrDesc').value.trim(), isPrivate: document.getElementById('nrPrivate').checked });
+    hideModal('createRepoModal');
+    document.getElementById('nrName').value = '';
+    document.getElementById('nrDesc').value = '';
+    loadRepos();
+  } catch (e) { alert('Error: ' + e.message); }
+}
+
+// ─── Issues ──────────────────────────────────
+
+async function loadIssues() {
+  if (!S.repo) return;
+  const el = document.getElementById('iList');
+  el.innerHTML = '<div class="loading"><span class="spinner"></span></div>';
+  try {
+    const items = await send('listIssues', { repo: S.repo, state: document.getElementById('iState').value });
+    if (!items?.length) { el.innerHTML = '<div class="empty">No issues</div>'; return; }
+    el.innerHTML = items.map(i => `<div class="item-card" onclick="viewIssue(${i.number})">
+      <div class="num">#${i.number}</div>
+      <div class="title">${esc(i.title)}</div>
+      <div class="meta"><span>${i.author?.login||'?'}</span><span>${timeAgo(i.createdAt)}</span><span>${i.comments||0} comments</span></div>
+      ${(i.labels||[]).map(l=>`<span class="label-tag" style="background:${l.color?'#'+l.color+'33':'var(--bg3)'};color:${l.color?'#'+l.color:'var(--text2)'}">${esc(l.name)}</span>`).join('')}
+    </div>`).join('');
+  } catch (e) { el.innerHTML = `<div class="empty">${esc(e.message)}</div>`; }
+}
+
+async function viewIssue(num) {
+  const el = document.getElementById('iDetail');
+  el.innerHTML = '<div class="loading"><span class="spinner"></span></div>';
+  document.querySelectorAll('#iList .item-card').forEach(c => c.classList.toggle('sel', c.querySelector('.num')?.textContent === '#'+num));
+  try {
+    const d = await send('viewIssue', { repo: S.repo, number: num });
+    const cm = d.commentList || [];
+    el.innerHTML = `
+      <div class="detail-header"><h2>${esc(d.title)} <span style="color:var(--text2);font-weight:400">#${d.number}</span></h2>
+      <div class="meta"><span style="color:${d.state==='OPEN'?'var(--green)':'var(--purple)'}">${d.state}</span> &middot; ${d.author?.login||'?'} &middot; ${timeAgo(d.createdAt)}</div></div>
+      <div class="detail-body">${renderMd(d.body||'(empty)')}</div>
+      ${cm.length?cm.map(c=>`<div class="comment"><span class="author">${esc(c.author)}</span><span class="time">${timeAgo(c.createdAt)}</span><div class="body">${renderMd(c.body)}</div></div>`).join(''):''}
+      <div class="reply-area"><textarea id="iReply" placeholder="Write a reply..."></textarea>
+      <div class="reply-actions">
+        ${S.aiReady?`<button class="ai" onclick="aiDraft('issue',${num})">AI Draft</button>`:''}<span id="iConf"></span><span style="flex:1"></span>
+        <button class="primary" onclick="postIssueComment(${num})">Reply</button>
+        ${d.state==='OPEN'?`<button class="danger" onclick="doCloseIssue(${num})">Close</button>`:`<button onclick="doReopenIssue(${num})">Reopen</button>`}
+      </div></div>`;
+  } catch (e) { el.innerHTML = `<div class="empty">${esc(e.message)}</div>`; }
+}
+
+async function postIssueComment(num) {
+  const body = document.getElementById('iReply').value.trim();
+  if (!body) return;
+  await send('commentIssue', { repo: S.repo, number: num, body });
+  viewIssue(num);
+}
+async function doCloseIssue(num) { await send('closeIssue', { repo: S.repo, number: num }); viewIssue(num); loadIssues(); }
+async function doReopenIssue(num) { await send('reopenIssue', { repo: S.repo, number: num }); viewIssue(num); loadIssues(); }
+
+// ─── PRs ─────────────────────────────────────
+
+async function loadPRs() {
+  if (!S.repo) return;
+  const el = document.getElementById('pList');
+  el.innerHTML = '<div class="loading"><span class="spinner"></span></div>';
+  try {
+    const items = await send('listPRs', { repo: S.repo, state: document.getElementById('pState').value });
+    if (!items?.length) { el.innerHTML = '<div class="empty">No PRs</div>'; return; }
+    el.innerHTML = items.map(p => `<div class="item-card" onclick="viewPR(${p.number})">
+      <div class="num">#${p.number}${p.isDraft?' <span style="color:var(--text2)">(Draft)</span>':''}</div>
+      <div class="title">${esc(p.title)}</div>
+      <div class="meta"><span>${p.author?.login||'?'}</span><span>${p.headRefName} &rarr; ${p.baseRefName}</span><span>${timeAgo(p.createdAt)}</span></div>
+    </div>`).join('');
+  } catch (e) { el.innerHTML = `<div class="empty">${esc(e.message)}</div>`; }
+}
+
+async function viewPR(num) {
+  const el = document.getElementById('pDetail');
+  el.innerHTML = '<div class="loading"><span class="spinner"></span></div>';
+  document.querySelectorAll('#pList .item-card').forEach(c => c.classList.toggle('sel', c.querySelector('.num')?.textContent?.startsWith('#'+num)));
+  try {
+    const d = await send('viewPR', { repo: S.repo, number: num });
+    const cm = d.commentList || [];
+    const sc = d.state==='OPEN'?'var(--green)':d.state==='MERGED'?'var(--purple)':'var(--red)';
+    el.innerHTML = `
+      <div class="detail-header"><h2>${esc(d.title)} <span style="color:var(--text2);font-weight:400">#${d.number}</span></h2>
+      <div class="meta"><span style="color:${sc}">${d.state}</span> &middot; ${d.author?.login||'?'} &middot; ${d.headRefName}&rarr;${d.baseRefName} &middot; +${d.additions||0} -${d.deletions||0} (${d.changedFiles||0} files)</div></div>
+      <div class="detail-body">${renderMd(d.body||'(empty)')}</div>
+      ${cm.length?cm.map(c=>`<div class="comment"><span class="author">${esc(c.author)}</span><span class="time">${timeAgo(c.createdAt)}</span><div class="body">${renderMd(c.body)}</div></div>`).join(''):''}
+      <div class="reply-area"><textarea id="pReply" placeholder="Write a comment..."></textarea>
+      <div class="reply-actions">
+        ${S.aiReady?`<button class="ai" onclick="aiDraft('pr',${num})">AI Draft</button>`:''}<span id="pConf"></span><span style="flex:1"></span>
+        <button class="primary" onclick="postPRComment(${num})">Comment</button>
+        ${d.state==='OPEN'?`<button class="primary" onclick="doMergePR(${num})">Merge</button><button class="danger" onclick="doClosePR(${num})">Close</button>`:''}
+      </div></div>`;
+  } catch (e) { el.innerHTML = `<div class="empty">${esc(e.message)}</div>`; }
+}
+
+async function postPRComment(num) { const b = document.getElementById('pReply').value.trim(); if (!b) return; await send('commentPR', { repo: S.repo, number: num, body: b }); viewPR(num); }
+async function doMergePR(num) { await send('mergePR', { repo: S.repo, number: num, method: 'merge' }); viewPR(num); loadPRs(); }
+async function doClosePR(num) { await send('closePR', { repo: S.repo, number: num }); viewPR(num); loadPRs(); }
+
+// ─── AI Draft ────────────────────────────────
+
+async function aiDraft(type, num) {
+  const tid = type==='issue'?'iReply':'pReply';
+  const cid = type==='issue'?'iConf':'pConf';
+  const ta = document.getElementById(tid);
+  const conf = document.getElementById(cid);
+  ta.value = 'Generating...'; ta.disabled = true;
+  conf.innerHTML = '<span class="spinner"></span>';
+  try {
+    const detail = await send(type==='issue'?'viewIssue':'viewPR', { repo: S.repo, number: num });
+    const res = await send('aiDraft', { type, title: detail.title, body: detail.body, comments: detail.commentList, repoName: S.repo });
+    ta.value = res.draft;
+    conf.innerHTML = `<span class="confidence ${res.confidence}">${res.confidence}</span>`;
+  } catch (e) { ta.value = ''; conf.textContent = ''; alert('AI error: '+e.message); }
+  ta.disabled = false; ta.focus();
+}
+
+// ─── Notifications ───────────────────────────
+
+const NI = { Issue:'<span style="color:var(--green)">&#9679;</span>', PullRequest:'<span style="color:var(--purple)">&#10542;</span>', Release:'<span style="color:var(--accent)">&#9830;</span>' };
+
+async function loadNotifs() {
+  const el = document.getElementById('notifList');
+  el.innerHTML = '<div class="loading"><span class="spinner"></span></div>';
+  try {
+    const items = await send('listNotifications');
+    setBadge(items?.length || 0);
+    if (!items?.length) { el.innerHTML = '<div class="empty">No unread notifications</div>'; return; }
+    el.innerHTML = items.map(n => `<div class="notif-item">
+      <div class="icon">${NI[n.type]||'&#9679;'}</div>
+      <div class="info"><div class="repo">${esc(n.repo)}</div><div class="title">${esc(n.title)}</div><div class="reason">${n.reason} &middot; ${timeAgo(n.updatedAt)}</div></div>
+      <button onclick="markRead('${n.id}',this.parentElement)">Read</button>
+    </div>`).join('');
+  } catch (e) { el.innerHTML = `<div class="empty">${esc(e.message)}</div>`; }
+}
+
+async function markRead(id, el) { await send('markRead', { id }); if (el) el.style.opacity = '0.3'; }
+async function markAllRead() { await send('markAllRead'); loadNotifs(); }
+
+// ─── Push ────────────────────────────────────
+
+async function pickFolder() { const p = await send('pickFolder'); if (p) document.getElementById('pushDir').value = p; }
+async function useWorkspace() { const p = await send('getWorkspaceDir'); if (p) document.getElementById('pushDir').value = p; }
+
+async function checkGit() {
+  const dir = document.getElementById('pushDir').value.trim(); if (!dir) return;
+  const el = document.getElementById('gitInfo');
+  el.innerHTML = '<div class="loading"><span class="spinner"></span></div>';
+  try {
+    const s = await send('gitStatus', { dir });
+    el.innerHTML = `<div class="branch">${esc(s.branch)}</div>
+      <div class="counts">${s.ahead>0?`<span style="color:var(--green)">${s.ahead} ahead</span> `:''}${s.behind>0?`<span style="color:var(--red)">${s.behind} behind</span> `:''}${s.ahead===0&&s.behind===0?'Up to date':''}</div>
+      ${s.status?`<pre>${esc(s.status)}</pre>`:'<pre style="color:var(--text2)">Clean</pre>'}`;
+    document.getElementById('pushBtn').disabled = false;
+  } catch (e) { el.innerHTML = `<span style="color:var(--red)">${esc(e.message)}</span>`; document.getElementById('pushBtn').disabled = true; }
+}
+
+async function doPush() {
+  const dir = document.getElementById('pushDir').value.trim(); if (!dir) return;
+  const log = document.getElementById('pushLog');
+  log.style.display = 'block'; log.innerHTML = 'Pushing...\n';
+  document.getElementById('pushBtn').disabled = true;
+  try {
+    const r = await send('gitPush', { dir });
+    log.innerHTML += `<span class="ok">${esc(typeof r==='string'?r:r?.message||'Done')}</span>\n`;
+  } catch (e) { log.innerHTML += `<span class="err">Error: ${esc(e.message)}</span>\n`; }
+  document.getElementById('pushBtn').disabled = false;
+  checkGit();
+}
+
+function doQuickSetup() {
+  const dir = document.getElementById('pushDir').value.trim();
+  if (!dir) { alert('Set directory first'); return; }
+  const n = dir.split('/').pop();
+  document.getElementById('qsName').value = `GeneralNoCodeDP/${n}`;
+  showModal('quickSetupModal');
+}
+
+async function execQuickSetup() {
+  const dir = document.getElementById('pushDir').value.trim();
+  const log = document.getElementById('pushLog');
+  log.style.display = 'block'; log.innerHTML = 'Setting up...\n';
+  hideModal('quickSetupModal');
+  try {
+    const r = await send('quickSetup', {
+      dir,
+      createRepo: true,
+      repoName: document.getElementById('qsName').value.trim(),
+      description: document.getElementById('qsDesc').value.trim(),
+      isPrivate: document.getElementById('qsPrivate').checked,
+      commitMessage: document.getElementById('qsMsg').value.trim() || 'Initial commit'
+    });
+    log.innerHTML += `<span class="ok">${esc(r?.message||'Done!')}</span>\n`;
+  } catch (e) { log.innerHTML += `<span class="err">Error: ${esc(e.message)}</span>\n`; }
+  checkGit();
+}
+
+// ─── Publish ─────────────────────────────────
+
+function populateRepoSelect() {
+  const sel = document.getElementById('pubRepo');
+  const cur = sel.value;
+  sel.innerHTML = '<option value="">Target Repo</option>' + S.repos.map(r => {
+    const nwo = r.owner.login+'/'+r.name;
+    return `<option value="${nwo}"${nwo===cur?' selected':''}>${nwo}</option>`;
+  }).join('');
+}
+
+function onRepoChange() {
+  // Auto-load existing README when repo changes
+}
+
+async function fetchExistingReadme() {
+  const repo = document.getElementById('pubRepo').value;
+  if (!repo) { alert('Select a repo first'); return; }
+  const editor = document.getElementById('pubEditor');
+  const res = document.getElementById('pubResult');
+  res.innerHTML = '<span class="spinner"></span> Loading...';
+  try {
+    const data = await send('fetchReadme', { repo });
+    if (data.content) {
+      editor.value = data.content;
+      updatePreview();
+      res.innerHTML = '<span style="color:var(--green)">Loaded existing README</span>';
+    } else {
+      res.innerHTML = '<span style="color:var(--text2)">No existing README found</span>';
+    }
+  } catch (e) { res.innerHTML = `<span style="color:var(--red)">Error: ${esc(e.message)}</span>`; }
+}
+
+function updatePreview() {
+  const md = document.getElementById('pubEditor').value;
+  const el = document.getElementById('pubPreview');
+  if (!md.trim()) { el.innerHTML = '<div class="empty" style="height:auto;">Live preview will appear here</div>'; return; }
+  el.innerHTML = renderFullMd(md);
+}
+
+// ─── Toolbar Actions ────────────────────────
+
+function getEditor() { return document.getElementById('pubEditor'); }
+
+function tbWrap(before, after) {
+  const ta = getEditor();
+  const start = ta.selectionStart, end = ta.selectionEnd;
+  const sel = ta.value.substring(start, end) || 'text';
+  ta.value = ta.value.substring(0, start) + before + sel + after + ta.value.substring(end);
+  ta.selectionStart = start + before.length;
+  ta.selectionEnd = start + before.length + sel.length;
+  ta.focus(); updatePreview();
+}
+
+function tbPrefix(prefix) {
+  const ta = getEditor();
+  const start = ta.selectionStart;
+  // Find line start
+  const lineStart = ta.value.lastIndexOf('\n', start - 1) + 1;
+  ta.value = ta.value.substring(0, lineStart) + prefix + ta.value.substring(lineStart);
+  ta.selectionStart = ta.selectionEnd = start + prefix.length;
+  ta.focus(); updatePreview();
+}
+
+function tbLink() {
+  const ta = getEditor();
+  const sel = ta.value.substring(ta.selectionStart, ta.selectionEnd) || 'link text';
+  const start = ta.selectionStart;
+  const ins = `[${sel}](url)`;
+  ta.value = ta.value.substring(0, start) + ins + ta.value.substring(ta.selectionEnd);
+  // Select "url" for easy replacement
+  ta.selectionStart = start + sel.length + 3;
+  ta.selectionEnd = start + sel.length + 6;
+  ta.focus(); updatePreview();
+}
+
+function tbImage() {
+  const ta = getEditor();
+  const start = ta.selectionStart;
+  const ins = '![alt text](image-url)';
+  ta.value = ta.value.substring(0, start) + ins + ta.value.substring(ta.selectionEnd);
+  ta.selectionStart = start + 12;
+  ta.selectionEnd = start + 21;
+  ta.focus(); updatePreview();
+}
+
+function tbCodeBlock() {
+  const ta = getEditor();
+  const sel = ta.value.substring(ta.selectionStart, ta.selectionEnd) || 'code here';
+  const start = ta.selectionStart;
+  const ins = '```\n' + sel + '\n```';
+  ta.value = ta.value.substring(0, start) + ins + ta.value.substring(ta.selectionEnd);
+  ta.selectionStart = start + 4;
+  ta.selectionEnd = start + 4 + sel.length;
+  ta.focus(); updatePreview();
+}
+
+function tbTable() {
+  const ta = getEditor();
+  const start = ta.selectionStart;
+  const ins = '\n| Column 1 | Column 2 | Column 3 |\n|----------|----------|----------|\n| Cell 1   | Cell 2   | Cell 3   |\n| Cell 4   | Cell 5   | Cell 6   |\n';
+  ta.value = ta.value.substring(0, start) + ins + ta.value.substring(ta.selectionEnd);
+  ta.selectionStart = start + 3;
+  ta.selectionEnd = start + 11;
+  ta.focus(); updatePreview();
+}
+
+// ─── Image Upload ───────────────────────────
+
+async function tbUploadImage() {
+  const repo = document.getElementById('pubRepo').value;
+  if (!repo) { alert('Select a target repo first — images will be uploaded there'); return; }
+  const status = document.getElementById('imgStatus');
+  status.textContent = 'Picking image...';
+  try {
+    const img = await send('pickImage');
+    if (!img) { status.textContent = ''; return; }
+    status.textContent = `Uploading ${img.name} (${(img.size/1024).toFixed(1)}KB)...`;
+    const filePath = `assets/images/${img.name}`;
+    const result = await send('uploadImage', { repo, filePath, base64: img.base64, message: `Upload ${img.name}` });
+    // Insert markdown image at cursor
+    const ta = getEditor();
+    const pos = ta.selectionStart;
+    const imgMd = `![${img.name}](${result.url})\n`;
+    ta.value = ta.value.substring(0, pos) + imgMd + ta.value.substring(ta.selectionEnd);
+    ta.selectionStart = ta.selectionEnd = pos + imgMd.length;
+    updatePreview();
+    status.textContent = `Uploaded ${img.name}`;
+    setTimeout(() => { status.textContent = ''; }, 3000);
+  } catch (e) { status.textContent = `Error: ${e.message}`; }
+}
+
+// ─── Publish Action ─────────────────────────
+
+async function doPublish() {
+  const content = document.getElementById('pubEditor').value;
+  if (!content.trim()) { alert('Write some content first'); return; }
+  const repo = document.getElementById('pubRepo').value;
+  if (!repo) { alert('Select a repo'); return; }
+  const fpath = document.getElementById('pubPath').value.trim() || 'README.md';
+  const msg = document.getElementById('pubMsg').value.trim() || 'Update file';
+  const res = document.getElementById('pubResult');
+  res.innerHTML = '<span class="spinner"></span> Publishing...';
+  document.getElementById('pubBtn').disabled = true;
+  try {
+    await send('updateFile', { repo, content, path: fpath, message: msg });
+    res.innerHTML = `<span style="color:var(--green)">Published ${esc(fpath)} to ${esc(repo)}</span>`;
+  } catch (e) { res.innerHTML = `<span style="color:var(--red)">Error: ${esc(e.message)}</span>`; }
+  document.getElementById('pubBtn').disabled = false;
+}
+
+// ─── Utils ───────────────────────────────────
+
+function esc(s) { return s==null?'':String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;'); }
+
+function timeAgo(d) {
+  if (!d) return '';
+  const m = Math.floor((Date.now()-new Date(d))/60000);
+  if (m<1) return 'just now'; if (m<60) return m+'m'; const h=Math.floor(m/60);
+  if (h<24) return h+'h'; const dy=Math.floor(h/24); if (dy<30) return dy+'d';
+  return new Date(d).toLocaleDateString();
+}
+
+function renderMd(t) {
+  if (!t) return '';
+  let h = esc(t);
+  h = h.replace(/```(\w*)\n([\s\S]*?)```/g,'<pre><code>$2</code></pre>');
+  h = h.replace(/`([^`]+)`/g,'<code>$1</code>');
+  h = h.replace(/\*\*(.+?)\*\*/g,'<strong>$1</strong>');
+  h = h.replace(/\*(.+?)\*/g,'<em>$1</em>');
+  h = h.replace(/^### (.+)$/gm,'<h4 style="margin:6px 0 3px">$1</h4>');
+  h = h.replace(/^## (.+)$/gm,'<h3 style="margin:8px 0 4px">$1</h3>');
+  h = h.replace(/\[([^\]]+)\]\(([^)]+)\)/g,'<a href="$2" target="_blank">$1</a>');
+  h = h.replace(/^- (.+)$/gm,'&bull; $1');
+  h = h.replace(/\n/g,'<br>');
+  return h;
+}
+
+function renderFullMd(t) {
+  if (!t) return '';
+  let h = esc(t);
+  // Code blocks (fenced)
+  h = h.replace(/```(\w*)\n([\s\S]*?)```/g, '<pre><code>$2</code></pre>');
+  // Inline code
+  h = h.replace(/`([^`\n]+)`/g, '<code>$1</code>');
+  // Images (before links)
+  h = h.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, '<img src="$2" alt="$1">');
+  // Links
+  h = h.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank">$1</a>');
+  // Bold + Italic
+  h = h.replace(/\*\*\*(.+?)\*\*\*/g, '<strong><em>$1</em></strong>');
+  h = h.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
+  h = h.replace(/\*(.+?)\*/g, '<em>$1</em>');
+  h = h.replace(/~~(.+?)~~/g, '<del>$1</del>');
+  // Headings
+  h = h.replace(/^#### (.+)$/gm, '<h4>$1</h4>');
+  h = h.replace(/^### (.+)$/gm, '<h3>$1</h3>');
+  h = h.replace(/^## (.+)$/gm, '<h2>$1</h2>');
+  h = h.replace(/^# (.+)$/gm, '<h1>$1</h1>');
+  // Horizontal rule
+  h = h.replace(/^---+$/gm, '<hr>');
+  // Blockquote
+  h = h.replace(/^&gt; (.+)$/gm, '<blockquote>$1</blockquote>');
+  // Tables
+  h = h.replace(/^(\|.+\|)\n(\|[-| :]+\|)\n((?:\|.+\|\n?)+)/gm, (_, header, sep, body) => {
+    const ths = header.split('|').filter(c=>c.trim()).map(c=>'<th>'+c.trim()+'</th>').join('');
+    const rows = body.trim().split('\n').map(row => {
+      const tds = row.split('|').filter(c=>c.trim()).map(c=>'<td>'+c.trim()+'</td>').join('');
+      return '<tr>'+tds+'</tr>';
+    }).join('');
+    return '<table><thead><tr>'+ths+'</tr></thead><tbody>'+rows+'</tbody></table>';
+  });
+  // Unordered list
+  h = h.replace(/^- (.+)$/gm, '<li>$1</li>');
+  h = h.replace(/((?:<li>.*<\/li>\n?)+)/g, '<ul>$1</ul>');
+  // Ordered list
+  h = h.replace(/^\d+\. (.+)$/gm, '<li>$1</li>');
+  // Paragraphs: double newline
+  h = h.replace(/\n\n/g, '</p><p>');
+  // Single newlines (not inside block elements)
+  h = h.replace(/\n/g, '<br>');
+  // Clean up
+  h = '<p>' + h + '</p>';
+  h = h.replace(/<p>\s*<(h[1-4]|hr|pre|ul|ol|table|blockquote)/g, '<$1');
+  h = h.replace(/<\/(h[1-4]|pre|ul|ol|table|blockquote)>\s*<\/p>/g, '</$1>');
+  h = h.replace(/<p>\s*<\/p>/g, '');
+  return h;
+}
+
+// ─── Boot ────────────────────────────────────
+init();
+</script>
+</body>
+</html>

+ 438 - 0
package-lock.json

@@ -0,0 +1,438 @@
+{
+  "name": "gh-hub",
+  "version": "0.1.0",
+  "lockfileVersion": 3,
+  "requires": true,
+  "packages": {
+    "": {
+      "name": "gh-hub",
+      "version": "0.1.0",
+      "dependencies": {
+        "@anthropic-ai/sdk": "^0.39.0"
+      },
+      "engines": {
+        "vscode": "^1.85.0"
+      }
+    },
+    "node_modules/@anthropic-ai/sdk": {
+      "version": "0.39.0",
+      "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.39.0.tgz",
+      "integrity": "sha512-eMyDIPRZbt1CCLErRCi3exlAvNkBtRe+kW5vvJyef93PmNr/clstYgHhtvmkxN82nlKgzyGPCyGxrm0JQ1ZIdg==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "^18.11.18",
+        "@types/node-fetch": "^2.6.4",
+        "abort-controller": "^3.0.0",
+        "agentkeepalive": "^4.2.1",
+        "form-data-encoder": "1.7.2",
+        "formdata-node": "^4.3.2",
+        "node-fetch": "^2.6.7"
+      }
+    },
+    "node_modules/@types/node": {
+      "version": "18.19.130",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz",
+      "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==",
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~5.26.4"
+      }
+    },
+    "node_modules/@types/node-fetch": {
+      "version": "2.6.13",
+      "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz",
+      "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==",
+      "license": "MIT",
+      "dependencies": {
+        "@types/node": "*",
+        "form-data": "^4.0.4"
+      }
+    },
+    "node_modules/abort-controller": {
+      "version": "3.0.0",
+      "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
+      "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+      "license": "MIT",
+      "dependencies": {
+        "event-target-shim": "^5.0.0"
+      },
+      "engines": {
+        "node": ">=6.5"
+      }
+    },
+    "node_modules/agentkeepalive": {
+      "version": "4.6.0",
+      "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz",
+      "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==",
+      "license": "MIT",
+      "dependencies": {
+        "humanize-ms": "^1.2.1"
+      },
+      "engines": {
+        "node": ">= 8.0.0"
+      }
+    },
+    "node_modules/asynckit": {
+      "version": "0.4.0",
+      "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+      "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+      "license": "MIT"
+    },
+    "node_modules/call-bind-apply-helpers": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
+      "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/combined-stream": {
+      "version": "1.0.8",
+      "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+      "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+      "license": "MIT",
+      "dependencies": {
+        "delayed-stream": "~1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.8"
+      }
+    },
+    "node_modules/delayed-stream": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+      "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=0.4.0"
+      }
+    },
+    "node_modules/dunder-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
+      "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "gopd": "^1.2.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-define-property": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
+      "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-errors": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
+      "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-object-atoms": {
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
+      "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/es-set-tostringtag": {
+      "version": "2.1.0",
+      "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
+      "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
+      "license": "MIT",
+      "dependencies": {
+        "es-errors": "^1.3.0",
+        "get-intrinsic": "^1.2.6",
+        "has-tostringtag": "^1.0.2",
+        "hasown": "^2.0.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/event-target-shim": {
+      "version": "5.0.1",
+      "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+      "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6"
+      }
+    },
+    "node_modules/form-data": {
+      "version": "4.0.5",
+      "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
+      "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
+      "license": "MIT",
+      "dependencies": {
+        "asynckit": "^0.4.0",
+        "combined-stream": "^1.0.8",
+        "es-set-tostringtag": "^2.1.0",
+        "hasown": "^2.0.2",
+        "mime-types": "^2.1.12"
+      },
+      "engines": {
+        "node": ">= 6"
+      }
+    },
+    "node_modules/form-data-encoder": {
+      "version": "1.7.2",
+      "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz",
+      "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==",
+      "license": "MIT"
+    },
+    "node_modules/formdata-node": {
+      "version": "4.4.1",
+      "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz",
+      "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==",
+      "license": "MIT",
+      "dependencies": {
+        "node-domexception": "1.0.0",
+        "web-streams-polyfill": "4.0.0-beta.3"
+      },
+      "engines": {
+        "node": ">= 12.20"
+      }
+    },
+    "node_modules/function-bind": {
+      "version": "1.1.2",
+      "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
+      "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
+      "license": "MIT",
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-intrinsic": {
+      "version": "1.3.0",
+      "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
+      "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
+      "license": "MIT",
+      "dependencies": {
+        "call-bind-apply-helpers": "^1.0.2",
+        "es-define-property": "^1.0.1",
+        "es-errors": "^1.3.0",
+        "es-object-atoms": "^1.1.1",
+        "function-bind": "^1.1.2",
+        "get-proto": "^1.0.1",
+        "gopd": "^1.2.0",
+        "has-symbols": "^1.1.0",
+        "hasown": "^2.0.2",
+        "math-intrinsics": "^1.1.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/get-proto": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
+      "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
+      "license": "MIT",
+      "dependencies": {
+        "dunder-proto": "^1.0.1",
+        "es-object-atoms": "^1.0.0"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/gopd": {
+      "version": "1.2.0",
+      "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
+      "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-symbols": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
+      "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/has-tostringtag": {
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
+      "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
+      "license": "MIT",
+      "dependencies": {
+        "has-symbols": "^1.0.3"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/ljharb"
+      }
+    },
+    "node_modules/hasown": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
+      "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
+      "license": "MIT",
+      "dependencies": {
+        "function-bind": "^1.1.2"
+      },
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/humanize-ms": {
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz",
+      "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==",
+      "license": "MIT",
+      "dependencies": {
+        "ms": "^2.0.0"
+      }
+    },
+    "node_modules/math-intrinsics": {
+      "version": "1.1.0",
+      "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
+      "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.4"
+      }
+    },
+    "node_modules/mime-db": {
+      "version": "1.52.0",
+      "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+      "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/mime-types": {
+      "version": "2.1.35",
+      "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+      "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+      "license": "MIT",
+      "dependencies": {
+        "mime-db": "1.52.0"
+      },
+      "engines": {
+        "node": ">= 0.6"
+      }
+    },
+    "node_modules/ms": {
+      "version": "2.1.3",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+      "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+      "license": "MIT"
+    },
+    "node_modules/node-domexception": {
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
+      "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
+      "deprecated": "Use your platform's native DOMException instead",
+      "funding": [
+        {
+          "type": "github",
+          "url": "https://github.com/sponsors/jimmywarting"
+        },
+        {
+          "type": "github",
+          "url": "https://paypal.me/jimmywarting"
+        }
+      ],
+      "license": "MIT",
+      "engines": {
+        "node": ">=10.5.0"
+      }
+    },
+    "node_modules/node-fetch": {
+      "version": "2.7.0",
+      "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+      "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+      "license": "MIT",
+      "dependencies": {
+        "whatwg-url": "^5.0.0"
+      },
+      "engines": {
+        "node": "4.x || >=6.0.0"
+      },
+      "peerDependencies": {
+        "encoding": "^0.1.0"
+      },
+      "peerDependenciesMeta": {
+        "encoding": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/tr46": {
+      "version": "0.0.3",
+      "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+      "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+      "license": "MIT"
+    },
+    "node_modules/undici-types": {
+      "version": "5.26.5",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
+      "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==",
+      "license": "MIT"
+    },
+    "node_modules/web-streams-polyfill": {
+      "version": "4.0.0-beta.3",
+      "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz",
+      "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==",
+      "license": "MIT",
+      "engines": {
+        "node": ">= 14"
+      }
+    },
+    "node_modules/webidl-conversions": {
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+      "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+      "license": "BSD-2-Clause"
+    },
+    "node_modules/whatwg-url": {
+      "version": "5.0.0",
+      "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+      "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+      "license": "MIT",
+      "dependencies": {
+        "tr46": "~0.0.3",
+        "webidl-conversions": "^3.0.0"
+      }
+    }
+  }
+}

+ 28 - 0
package.json

@@ -0,0 +1,28 @@
+{
+  "name": "gh-hub",
+  "displayName": "GH-Hub",
+  "description": "GitHub management panel for VS Code — repos, issues, PRs, push, AI replies",
+  "version": "0.2.0",
+  "publisher": "VisualLogic-AI",
+  "engines": { "vscode": "^1.74.0" },
+  "categories": ["Other"],
+  "activationEvents": [
+    "onCommand:gh-hub.open",
+    "onCommand:gh-hub.pushCurrent",
+    "onCommand:gh-hub.publishReadme"
+  ],
+  "main": "./extension.js",
+  "contributes": {
+    "commands": [
+      { "command": "gh-hub.open", "title": "GH-Hub: Open Dashboard" },
+      { "command": "gh-hub.pushCurrent", "title": "GH-Hub: Push Current Project" },
+      { "command": "gh-hub.publishReadme", "title": "GH-Hub: Publish File as README" }
+    ],
+    "keybindings": [
+      { "command": "gh-hub.open", "key": "ctrl+shift+g", "mac": "cmd+shift+g" }
+    ]
+  },
+  "dependencies": {
+    "@anthropic-ai/sdk": "^0.39.0"
+  }
+}