diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 16690de2..6a52ea84 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -26,25 +26,3 @@ jobs: - name: Unit tests run: npm run test:unit - - name: Command surface drift check - run: npm run check:command-surface - - public_release_audit: - runs-on: ubuntu-latest - timeout-minutes: 10 - needs: [unit] - steps: - - name: Checkout - uses: actions/checkout@v4 - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: "20" - cache: "npm" - - - name: Install - run: npm ci - - - name: Public release audit (non-destructive) - run: npm run audit:public-release diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 76377e5f..c8969179 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -33,9 +33,6 @@ jobs: - name: Unit tests run: npm run test:unit - - name: Command surface drift check - run: npm run check:command-surface - tauri_windows: name: tauri build (windows) if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch' diff --git a/.gitignore b/.gitignore index 361edb4f..bce22963 100644 --- a/.gitignore +++ b/.gitignore @@ -64,10 +64,3 @@ playwright-report/ # Diff viewer cache (contains derived data; never commit) diff-viewer/cache/ - -# Debug/temp scripts -inspect-layout.js -a.out - -# Local backups -claude_backups/ diff --git a/CLAUDE.md b/CLAUDE.md index 434f1970..94dfd410 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,15 +36,7 @@ Tier tagging persistence: ## Launching Agents from Trello -### Board-to-Repo Mapping -**CRITICAL:** Always check `~/.claude/TRELLO_BOARDS.md` for the full board/list/repo mapping. Do NOT assume a board maps to a specific repo - look it up! - -| Board | Board ID | Repo Path | Repo Type | -|-------|----------|-----------|-----------| -| Zoo Hytopia | `691e5516c77f3e9c9fd89f61` | `~/GitHub/games/hytopia/zoo-game/` | `hytopia-game` | -| Arcade World | `694a07bae349c125d4568094` | `~/GitHub/games/hytopia/games/hytopia-2d-game-test/` | `hytopia-game` | - -Each board has its own **AB T3 Que**, **Doing**, and **Test** lists - IDs differ per board. Always look up the correct list ID from `TRELLO_BOARDS.md`. +**Zoo Hytopia board:** `` | **Tier-3 queue list:** `` **Get card with agent field:** ```bash @@ -64,7 +56,6 @@ curl -sS "https://api.trello.com/1/cards/CARD_ID/customFieldItems?key=$KEY&token - Claude: `claude --dangerously-skip-permissions` **Launch sequence (MUST follow):** -0. **CHECK ACTIVE WORKSPACE FIRST**: `GET /api/workspaces/active` β€” add worktrees to the workspace the user has open, NOT a random one 1. Remove all worktrees: `POST /api/workspaces/remove-worktree` for each 2. Re-add worktrees with tier: `POST /api/workspaces/add-mixed-worktree` (include `startTier`) 3. Start agent: send launch command + `\r` @@ -75,7 +66,7 @@ curl -sS "https://api.trello.com/1/cards/CARD_ID/customFieldItems?key=$KEY&token **Add worktree with tier:** ```bash -curl -sS -X POST http://localhost:$PORT/api/workspaces/add-mixed-worktree \ +curl -sS -X POST http://localhost:3000/api/workspaces/add-mixed-worktree \ -H "Content-Type: application/json" \ -d '{ "workspaceId": "zoo-shrimp-game", @@ -90,7 +81,7 @@ curl -sS -X POST http://localhost:$PORT/api/workspaces/add-mixed-worktree \ **Link Trello card to session (task record):** ```bash -curl -sS -X PUT "http://localhost:$PORT/api/process/task-records/session:zoo-game-work1-claude" \ +curl -sS -X PUT "http://localhost:3000/api/process/task-records/session:zoo-game-work1-claude" \ -H "Content-Type: application/json" \ -d '{ "tier": 3, @@ -108,92 +99,6 @@ curl -sS -X PUT "http://localhost:$PORT/api/process/task-records/session:zoo-gam - Stop-session doesn't fully clear - use remove-worktree + re-add - Sessions are paired (claude+server) - remove both via worktree -### Trello β†’ Codex Batch Launch Process - -When user says "launch all cards from [list] as Codexes": - -**1. Get the correct list ID:** -```bash -KEY=$(awk -F= '/^TRELLO_API_KEY=/{print $2}' ~/.trello-credentials | tr -d '\r\n[:space:]') -TOKEN=$(awk -F= '/^TRELLO_TOKEN=/{print $2}' ~/.trello-credentials | tr -d '\r\n[:space:]') -curl -fsS "https://api.trello.com/1/boards/BOARD_ID/lists?key=${KEY}&token=${TOKEN}" | jq -r '.[] | "\(.id) | \(.name)"' -``` - -**2. Get ALL cards with FULL descriptions:** -```bash -curl -fsS "https://api.trello.com/1/lists/LIST_ID/cards?key=${KEY}&token=${TOKEN}&fields=id,name,desc" > /tmp/trello-cards.json -# Verify desc lengths (list endpoint may truncate): -jq -r '.[] | {name: .name[0:60], desc_len: (.desc | length)}' /tmp/trello-cards.json -``` - -**3. Add worktrees to current workspace:** -```bash -for i in $(seq 1 N); do - curl -sS -X POST "http://localhost:$PORT/api/workspaces/add-mixed-worktree" \ - -H "Content-Type: application/json" \ - -d '{"workspaceId": "WORKSPACE", "repositoryPath": "REPO_PATH", "repositoryType": "hytopia-game", "repositoryName": "REPO_NAME", "worktreeId": "work'$i'", "startTier": 3}' -done -``` - -**4. For each card, launch Codex then send prompt:** -```bash -SESSION_ID="REPONAME-workN-claude" -# Launch Codex -curl -sS -X POST "http://localhost:$PORT/api/commander/send-to-session" \ - -H "Content-Type: application/json" \ - -d "{\"sessionId\": \"$SESSION_ID\", \"input\": \"\u0015codex -m gpt-5.3-codex -c model_reasoning_effort=xhigh --dangerously-bypass-approvals-and-sandbox\"}" -sleep 1 -curl -sS -X POST "http://localhost:$PORT/api/commander/send-to-session" \ - -H "Content-Type: application/json" \ - -d "{\"sessionId\": \"$SESSION_ID\", \"input\": \"\r\"}" -sleep 3 # wait for Codex init (only needs 2-3s) - -# Send VERBATIM title + desc + system instructions AFTER -PROMPT="${CARD_TITLE}\n\n${CARD_DESC}\n\n---\nSYSTEM INSTRUCTIONS:\n1. git fetch origin master && git checkout master && git pull\n2. git checkout -b feature/BRANCH_SLUG\n3. Read CLAUDE.md and CODEBASE_DOCUMENTATION.md first\n4. Implement everything above verbatim\n5. Clean surgical code, minimal diff\n6. Automated tests following existing patterns\n7. NEVER squash merge\n8. Commit and push regularly\n9. gh pr create when done, include PR link\n10. Run existing tests" - -curl -sS -X POST "http://localhost:$PORT/api/commander/send-to-session" \ - -H "Content-Type: application/json" \ - --data-binary @- << EOF -{"sessionId": "$SESSION_ID", "input": $(echo "$PROMPT" | jq -Rs .)} -EOF -sleep 1 -curl -sS -X POST "http://localhost:$PORT/api/commander/send-to-session" \ - -H "Content-Type: application/json" \ - -d "{\"sessionId\": \"$SESSION_ID\", \"input\": \"\r\"}" -``` - -**Batch launch key rules:** -- NEVER summarize card title or description - paste VERBATIM -- Use `gpt-5.3-codex` model with `xhigh` reasoning -- System instructions go AFTER the card content -- Two-request pattern: text first, then `\r` separately -- 3s sleep for Codex init (it initializes in ~2-3s, not 15) -- Use `\u0015` (Ctrl+U) before Codex command to clear line - -## Codex CLI Reference - -### Launch commands -- Claude: `claude --dangerously-skip-permissions` -- Codex: `codex --dangerously-bypass-approvals-and-sandbox` -- Codex with explicit model: `codex -m gpt-5.3-codex -c model_reasoning_effort=xhigh --dangerously-bypass-approvals-and-sandbox` - -### Codex Upgrade Issues - -**ENOTEMPTY error on npm upgrade:** -```bash -rm -rf ~/.nvm/versions/node/v24.9.0/lib/node_modules/@openai/codex && npm i -g @openai/codex@latest -``` - -**"Model does not exist" errors** β€” Codex needs upgrading: -```bash -npm i -g @openai/codex@latest -``` - -### Codex config location -- Config: `~/.codex/config.toml` -- Global instructions: `~/.codex/AGENTS.md` -- Fallback filenames (set in config): reads `CLAUDE.md` if no `AGENTS.md` - ## 🚨 STOP! DO THIS FIRST BEFORE ANYTHING ELSE! 🚨 ### THE VERY FIRST THING YOU MUST DO (NO EXCEPTIONS): @@ -210,8 +115,6 @@ Because `main` is usually checked out in the `master/` worktree, **do not try to **CRITICAL SAFETY:** If you are working in `claude-orchestrator-dev/`, **do not edit, pull, or run commands in the `master/` folder** unless explicitly requested β€” that instance may be running on port **3000**. -**ALSO CRITICAL:** If Commander Claude is running FROM `master/`, **NEVER edit files in `master/`**. Even if you revert changes, nodemon will detect the file change and restart the production server, which crashes all active sessions. ALL code changes go in `claude-orchestrator-dev/` on a feature branch, then PR into main. The ONLY exception is if the user explicitly asks you to edit production. - **TEST SAFETY (ports):** - Never use port `3000` for dev/test runs. - Use `npm run test:e2e:safe` (defaults to a dedicated port) for Playwright. @@ -235,7 +138,6 @@ Because `main` is usually checked out in the `master/` worktree, **do not try to ## Git Workflow Notes - Always work on fresh branches from updated main - If `git fetch origin main:main` fails, use `git fetch origin main && git checkout -b feature/name origin/main` -- Never provide delivery estimates in weeks; provide dependency-ordered execution slices instead. ## Code Style Guidelines @@ -274,32 +176,12 @@ Because `main` is usually checked out in the `master/` worktree, **do not try to - **WorktreeHelper**: Git worktree operations (`server/worktreeHelper.js`) - **NotificationService**: System notifications (`server/notificationService.js`) - **CommanderService**: Top-Level AI orchestration terminal (`server/commanderService.js`) -- **CommandHistoryService**: Terminal autosuggestions via shell history (`server/commandHistoryService.js`) - **Tauri App**: Native desktop application (`src-tauri/`) - **Diff Viewer**: Advanced code review tool (`diff-viewer/`) ## Commander Claude (Top-Level AI) -Commander Claude is a special Claude Code instance that runs from the orchestrator `master/` directory with knowledge of the entire system. When you ARE Commander Claude (running in this directory or launched from the Commander panel), you have these capabilities. - -**IMPORTANT:** When you first start, greet the user with: -> Commander Claude reporting for duty, sir! - -**Read the full Commander instructions:** -```bash -cat ~/GitHub/tools/automation/claude-orchestrator/master/COMMANDER_CLAUDE.md -``` - -### Port Detection (MANDATORY β€” do this first) - -The orchestrator port is NOT hardcoded. It comes from `.env` in your working directory: -```bash -PORT=$(grep ORCHESTRATOR_PORT .env | cut -d= -f2) -# Production (master/) = typically 3000, Dev = typically 4000 -# All API examples below use $PORT β€” resolve it before running commands -``` - -**All `curl` examples in this file use `$PORT`.** Never assume 3000 or 4000. +Commander Claude is a special Claude Code instance that runs from the orchestrator directory with knowledge of the entire system. When you ARE Commander Claude (running in this directory), you have these capabilities: ### What Commander Can Do 1. **View All Sessions**: See all active Claude sessions across all workspaces @@ -325,81 +207,12 @@ POST /api/commander/start-claude { mode: 'fresh'|'continue'|'resume', yolo: tru # Send input to Commander terminal POST /api/commander/input { input: "text to send" } -# Get active workspace (which workspace the UI is showing) -GET /api/workspaces/active -# Returns: { id: "workspace-id", name: "Workspace Name" } -# Falls back to persisted config if in-memory state is null - -# View all sessions β€” returns {"sessions":[...]} NOT bare array! +# View all sessions GET /api/commander/sessions -# jq: use '.sessions[]' not '.[]' # Send to another session POST /api/commander/send-to-session { sessionId: "...", input: "..." } - -# System Recommendations (missing tools, suggested installs) -GET /api/recommendations # returns {"items":[...]} -POST /api/recommendations # {"package","reason","installCmd","category"} -PATCH /api/recommendations/:id # {"status":"installed"|"dismissed"} -DELETE /api/recommendations/:id # remove entirely -``` - -### Logging Missing Tools -When a command fails with "not found", POST a recommendation so the user sees it in the UI πŸ”§ badge: -```bash -curl -sS -X POST http://localhost:$PORT/api/recommendations \ - -H "Content-Type: application/json" \ - -d '{"package":"dos2unix","reason":"CRLF fix for WSL scripts","installCmd":"sudo apt-get install -y dos2unix","category":"apt"}' -``` - -### Quick Orchestrator Commands - -**Focus a Worktree** (show only one worktree's terminals): -```bash -curl -sS -X POST http://localhost:$PORT/api/commander/execute \ - -H "Content-Type: application/json" \ - -d '{"command": "focus-worktree", "params": {"worktreeId": "work1"}}' -``` - -**Show All Worktrees** (unfocus/reset view): -**NOTE:** The `show-all-worktrees` API command is BROKEN (calls non-existent method). -Use the "View All" button in the UI (bottom-left under worktrees list) instead. - -**Highlight a Worktree** (visual highlight without hiding others): -```bash -curl -sS -X POST http://localhost:$PORT/api/commander/execute \ - -H "Content-Type: application/json" \ - -d '{"command": "highlight-worktree", "params": {"worktreeId": "work1"}}' -``` - -**List All Workspaces:** -```bash -curl -sS http://localhost:$PORT/api/workspaces | jq '.[].name' -``` - -**Get Workspace Details** (including worktrees): -```bash -curl -sS http://localhost:$PORT/api/workspaces | jq '.[] | select(.name == "Zoo Game")' -``` - -**Switch to Different Workspace:** -Use Socket.IO event `switch-workspace` with `workspaceId` - handled via the UI primarily. - -**Add Worktree to Workspace** (CORRECT API FORMAT): -**DO NOT use `path` or `worktreePath`** - the API expects these specific parameters: -```bash -curl -sS -X POST http://localhost:$PORT/api/workspaces/add-mixed-worktree \ - -H "Content-Type: application/json" \ - -d '{ - "workspaceId": "workspace-id", - "repositoryPath": "/home//GitHub/games/hytopia/zoo-game", - "repositoryType": "hytopia-game", - "repositoryName": "zoo-game", - "worktreeId": "work1", - "startTier": 3 - }' ``` -The worktreePath is computed internally as `repositoryPath + worktreeId`. ### Project Workspaces Location Workspaces are stored in `~/.orchestrator/workspaces/`. Each workspace has: @@ -702,7 +515,6 @@ SERVICES: Modular service architecture with clear interfaces 14. **Undefined config spread**: Handle missing gameModes/commonFlags with `{ ...(result[key] || {}), ...override[key] }` pattern 15. **XTerm rendering race**: Wrap fitTerminal() in requestAnimationFrame() to allow renderer initialization 16. **Repository name extraction**: For mixed-repo workspaces, use workspace config's terminal.repository.name, not session ID parsing -17. **ALWAYS check active workspace first**: Before adding worktrees or launching agents, call `GET /api/workspaces/active` to find which workspace the user currently has open. Add worktrees to THAT workspace β€” never guess or pick a workspace by name ## Development Setup - Two Isolated Instances diff --git a/CODEBASE_DOCUMENTATION.md b/CODEBASE_DOCUMENTATION.md index eadc9144..c3f6896b 100644 --- a/CODEBASE_DOCUMENTATION.md +++ b/CODEBASE_DOCUMENTATION.md @@ -17,7 +17,6 @@ FRONTEND: client/app.js, client/terminal.js - Web client NATIVE: src-tauri/src/main.rs - Native desktop app CONFIG: config.json, package.json - Configuration files DIFF: diff-viewer/ - Advanced diff viewer component -PLANS: PLANS/ - Date-stamped planning + implementation notes ``` ## Core Systems (Start Here) @@ -33,14 +32,10 @@ server/index.js - Express server with Socket.IO server/sessionManager.js - Terminal session lifecycle management β”œβ”€ Manages: PTY processes, session tracking, cleanup β”œβ”€ Key methods: createSession(), destroySession(), getActiveSessions() -β”œβ”€ Cleanup hardening: closing sessions sends process-tree SIGTERM and a grace-timed SIGKILL fallback by PTY pid to reduce orphaned agent processes -β”œβ”€ Stale-agent cleanup: when status detection sees an explicit shell/no-agent prompt, recovery `lastAgent` markers are cleared to keep sidebar status accurate (`no-agent` vs `busy/waiting`) -β”œβ”€ Status model: periodic status re-evaluation prevents stale "busy" lights after output quiets down └─ Uses: node-pty for terminal emulation server/statusDetector.js - Claude Code session monitoring β”œβ”€ Detects: Claude sessions, branch changes, status updates -β”œβ”€ Busy/idle heuristics: tool/typing signals are recency-gated to avoid stale "busy forever" states β”œβ”€ Events: session-detected, branch-changed, status-updated └─ Polling: Configurable intervals for status checks @@ -57,32 +52,6 @@ server/claudeVersionChecker.js - Claude Code version detection server/tokenCounter.js - Token usage tracking (if applicable) server/userSettingsService.js - User preferences and settings management server/sessionRecoveryService.js - Session recovery state persistence (CWD, agents, conversations) -β”œβ”€ Recovery filtering: stale/non-configured session entries are pruned when requested by workspace-scoped APIs -β”œβ”€ Agent clearing: `clearAgent()` resets stale `lastAgent` markers when a Claude/Codex terminal falls back to plain shell -└─ Recovery metadata: recovery payload includes configured terminal/worktree counts for UI context -server/threadService.js - Workspace/project thread persistence (`~/.orchestrator/threads.json`) -β”œβ”€ Thread identity: active-thread de-dup scopes by workspace + worktree + repository context -β”œβ”€ Project identity: `projectId` is repository-scoped (`repo-path:*` / `repo-name:*`) instead of workspace-scoped when repository context is available -β”œβ”€ Repository normalization: thread/worktree creation normalizes `.../master` and `.../workN` paths to repository root -β”œβ”€ New chat reuse: thread creation prefers an existing repo worktree without an active thread before allocating a new `workN` -β”œβ”€ Project aggregation: `listProjects()` returns repository-level chat rollups across one/many workspaces -└─ Lifecycle: create/list/close/archive + session association updates -server/projectBoardService.js - Local projects kanban board persistence (`~/.orchestrator/project-board.json`) + APIs (`GET /api/projects/board`, `POST /api/projects/board/move`, `POST /api/projects/board/patch`) -server/discordIntegrationService.js - Discord queue orchestration bridge (Services workspace ensure/start, signed queue verification, invocation idempotency, JSONL audit log for processing dispatch/replay/fail paths) -server/intentHaikuService.js - Session intent summarizer for context-switch hints (optional Anthropic Haiku model, heuristic fallback) -server/threadWorktreeSelection.js - Repository/worktree normalization + reuse-first candidate selection for thread creation -server/policyService.js - Role/action policy checks (viewer/operator/admin) for sensitive APIs + command execution -server/policyBundleService.js - Policy template catalog + bundle export/import for team governance profiles -server/pluginLoaderService.js - Plugin manifest validation/compatibility, command registration safety, and client slot metadata -server/agentProviderService.js - Provider abstraction layer for Claude/Codex/future agents (sessions, resume plans, history search, transcript fetch) -server/workspaceServiceStackService.js - Workspace service-stack manifest normalization/validation (services, env, restart policy, healthchecks) -server/configPromoterService.js - Team/shared service-stack baseline promotion + attach/resolve with optional signature verification -server/encryptedStore.js - Reusable AES-256-GCM encrypted JSON store helper for shared config artifacts -server/serviceStackRuntimeService.js - Workspace service-stack runtime supervisor (start/stop/restart, desired state, auto-restart, health checks) -server/auditExportService.js - Redacted audit export across activity + scheduler logs (JSON/CSV) -server/networkSecurityPolicy.js - Bind-host/auth safety policy helpers (loopback defaults + LAN auth guardrails) -server/processTelemetryBenchmarkService.js - Release benchmark metrics (onboarding/runtime/review), snapshot comparisons, release-note markdown generation -server/projectTypeService.js - Project taxonomy loader/validator for categoryβ†’frameworkβ†’template metadata (`config/project-types.json`) ``` ### Multi-Workspace System (Core Feature) @@ -102,7 +71,6 @@ server/workspaceTypes.js - Workspace type definitions server/worktreeHelper.js - Git worktree operations wrapper β”œβ”€ Operations: Create, delete, manage git worktrees -β”œβ”€ Bootstrap helper: `createProjectWorktrees({ projectPath, count, baseBranch })` for initial `workN` creation β”œβ”€ Integration: Seamless workspace-worktree coordination └─ Safety: Path validation and cleanup handling ``` @@ -142,7 +110,7 @@ Config Structure: } client/app.js - Config pre-fetching & caching -β”œβ”€ Methods: prefetchWorktreeConfigs(), fetchCascadedConfig(), ensureProjectTypeTaxonomy() +β”œβ”€ Methods: prefetchWorktreeConfigs(), fetchCascadedConfig() β”œβ”€ Cache: Map for worktree-specific configs └─ Extract: extractRepositoryName() from workspace config ``` @@ -154,12 +122,6 @@ client/app.js - Config pre-fetching & caching client/app.js - Main client application β”œβ”€ Manages: UI state, socket connections, terminal grid β”œβ”€ Features: 16-terminal layout, real-time updates, session switching -β”œβ”€ Command Palette: header `⌘ Commands` button + `Ctrl/Cmd+K` searchable command launcher for command-catalog actions -β”œβ”€ Intent hints: compact "intent haiku" strip above each agent terminal, refreshed from `POST /api/sessions/intent-haiku` -β”œβ”€ Projects + Chats automation: `project-chats-new` Commander/voice action supports explicit workspace + repository targeting -β”œβ”€ Projects + Chats list: repository-first aggregation (project-centric view) while preserving workspace context for mixed workspaces -β”œβ”€ Projects + Chats data source: prefers server-aggregated repository projects from `GET /api/thread-projects` with client fallback aggregation -β”œβ”€ Status UI: visual state mapping for `busy`, `waiting`, `ready-new`, and `no-agent` └─ Dependencies: Socket.IO client, terminal emulation client/terminal.js - Terminal component implementation @@ -176,33 +138,16 @@ client/workspace-wizard.js - Workspace creation wizard β”œβ”€ Types: Single-repo, mixed-repo, and custom configurations └─ Integration: Worktree creation and template application -client/greenfield-wizard.js - New-project wizard (greenfield creation flow) -β”œβ”€ Uses project taxonomy categories before rendering -β”œβ”€ Calls `orchestrator.createProjectWorkspace(options)` to centralize socket + REST fallback (`POST /api/projects/create-workspace`) -β”œβ”€ Category β†’ framework β†’ template drilldown based on taxonomy relationships -β”œβ”€ GitHub controls: supports optional local-only creation (`createGithub=false`), explicit repo target (`owner/repo` or URL), and optional GitHub org/user prefix -β”œβ”€ Workspace-context suggestion (repo type -> recommended template/framework defaults) -└─ Full-screen wizard UI for project scaffolding + workspace creation - -client/projects-board.js - Projects kanban board modal (Archive/Maybe One Day/Backlog/Active/Ship Next/Done; drag/drop + re-order; collapsible columns; live tag; hide forks; persists via `/api/projects/board`) - client/workspace-tab-manager.js - Multi-workspace tab management (NEW) β”œβ”€ Features: Browser-like tabs for multiple workspaces β”œβ”€ Manages: Tab creation, switching, state preservation β”œβ”€ XTerm lifecycle: Proper hide/show with fit() handling β”œβ”€ Notifications: Badge counts for inactive tabs -└─ Keyboard shortcuts: Alt+←/β†’, Alt+W, Alt+N, Alt+Shift+N, Alt+1-9 +└─ Keyboard shortcuts: Ctrl+Tab, Ctrl+W, Ctrl+T, Ctrl+1-9 client/styles/tabs.css - Tab bar styling β”œβ”€ Features: Tab UI, badges, animations └─ Responsive: Mobile and desktop layouts - -client/styles/projects-board.css - Projects Board modal styling - -client/plugin-host.js - Client plugin runtime for UI slots/actions -β”œβ”€ Loads: `/api/plugins/client-surface` slot actions with cache/refresh support -β”œβ”€ Exposes: `window.orchestratorPluginHost` -└─ Supports actions: open_url, open_route, copy_text, commander_action ``` ### Tabbed Workspace System (NEW) @@ -249,19 +194,11 @@ TabState Structure: - Click "+" button to open new workspace - Click tab to switch - Click "Γ—" to close tab (confirms if terminals active) -- `Ctrl/Cmd+K` opens the command palette for quick command execution - Alt+← / Alt+β†’ to cycle tabs (previous/next) - Alt+1-9 to jump to specific tab - Alt+N for new workspace -- Alt+Shift+N for full-screen New Project wizard - Alt+W to close current tab -Dashboard notes: -- "Create Workspace" card remains for workspace-only setup -- "New Project" card opens the greenfield wizard directly -- Terminal headers include a `✨` quick action to open the New Project wizard from any session -- Recovery prompt explicitly separates "recoverable sessions" from total configured worktree/terminal counts - ``` ### Native Desktop App (Tauri) @@ -297,12 +234,6 @@ templates/launch-settings/ - Workspace configuration templates scripts/migrate-to-workspaces.js - Migration script for legacy workspaces β”œβ”€ Converts: Old workspace format to new multi-workspace format └─ Safety: Backup and rollback capabilities - -scripts/public-release-audit.js - Public-release safety audit automation -β”œβ”€ Checks: tracked cache/DB artifacts, public-doc path hygiene, loopback/auth defaults -└─ Optional: full-history gitleaks scan (`--history-secrets`) - -scripts/create-project.js - Taxonomy-driven project scaffold generator (template/project-kit source resolution, optional post-create hooks, git init, optional GitHub remote, worktree bootstrap via WorktreeHelper) ``` ## Advanced Diff Viewer Component @@ -356,7 +287,6 @@ git-command: {command, args} - Execute git command switch-workspace: {workspaceId} - Switch to different workspace create-workspace: {config} - Create new workspace get-workspaces: {} - Request workspace list -create-new-project: {name, category, template, ...} - Create project scaffold + workspace in one socket action close-tab: {tabId} - Close workspace tab and cleanup sessions (NEW) ``` @@ -447,39 +377,8 @@ POST /api/workspaces - Create new workspace PUT /api/workspaces/:id - Update workspace configuration DELETE /api/workspaces/:id - Delete workspace POST /api/workspaces/:id/switch - Switch to workspace -POST /api/workspaces/remove-worktree - Remove worktree from workspace config (mixed terminal arrays and numeric `terminals.pairs` modes), close linked sessions, prune matching recovery orphans even when config entry is already missing, keep files on disk -GET /api/threads - List project/workspace chats (`workspaceId` required) -GET /api/thread-projects - List repository-level chat projects aggregated from threads (optionally `workspaceId` scoped) -POST /api/threads - Create thread + ensure mixed worktree/session context -POST /api/threads/create - Alias for thread creation API used by Projects + Chats shell (idempotent for existing worktrees/sessions) -POST /api/threads/:id/close - Mark thread closed and close linked sessions -POST /api/threads/:id/archive - Archive thread (hidden unless includeArchived=true) -GET /api/project-types - Full project taxonomy (categories/frameworks/templates + metadata) -GET /api/project-types/categories - Project categories with resolved base paths -GET /api/project-types/frameworks?categoryId=... - Framework catalog (optionally scoped by category) -GET /api/project-types/templates?frameworkId=...&categoryId=... - Template catalog (optionally scoped) -POST /api/projects/create-workspace - Create project scaffold + matching workspace in one request -GET /api/discord/status - Discord queue + services health/status (counts + signature status); endpoint can be gated by `DISCORD_API_TOKEN` -POST /api/discord/ensure-services - Ensure Services workspace/session bootstrap; accepts optional `dangerousModeOverride` (gated by `DISCORD_ALLOW_DANGEROUS_OVERRIDE`) -POST /api/discord/process-queue - Dispatch queue processing prompt with optional `Idempotency-Key`/`idempotencyKey`, queue signature verification, idempotent replay, audit logging, and per-endpoint rate limiting -POST /api/sessions/intent-haiku - Generate <=200 char intent summary for an active Claude/Codex session -GET /api/greenfield/categories - Greenfield category list (taxonomy-backed) -POST /api/greenfield/detect-category - Infer category from description (taxonomy keyword matching) GET /api/user-settings - Get user preferences PUT /api/user-settings - Update user preferences - -GET /api/process/telemetry/benchmarks - Live + snapshot benchmark rows for onboarding/runtime/review comparisons -POST /api/process/telemetry/benchmarks/snapshots - Capture a named benchmark snapshot for release tracking -GET /api/process/telemetry/benchmarks/release-notes - Build markdown release notes comparing current vs baseline benchmark -GET /api/policy/templates - Built-in team governance policy templates -POST /api/policy/bundles/export - Export policy bundle (template/current/custom) for sharing -POST /api/policy/bundles/import - Apply policy bundle (replace/merge) into global settings -GET /api/audit/export?signed=1 - Signed audit export (HMAC-SHA256; requires signing enabled + secret) -GET /api/agent-providers - List registered agent providers and capabilities -GET /api/agent-providers/:providerId/sessions - List provider sessions from SessionManager -POST /api/agent-providers/:providerId/resume-plan - Build provider-specific resume command/config plan -GET /api/agent-providers/:providerId/history/search - Provider-scoped history search (conversation index source-aware) -GET /api/agent-providers/:providerId/history/:id - Provider-scoped transcript retrieval ``` ### WebSocket Events @@ -538,5 +437,16 @@ LOGGING: Winston-based structured logging with rotation 9. **Mixed-repo workspaces**: Terminal naming must avoid conflicts between repos 10. **Template validation**: Always validate workspace templates against schemas + +## First-Run Dependency Onboarding (Windows) + +``` +server/setupActionService.js - Defines setup actions and launches PowerShell installers +server/index.js - Routes: GET /api/setup-actions, POST /api/setup-actions/run +client/app.js - Guided dependency onboarding steps + diagnostics integration +client/index.html - Dependency onboarding modal markup + launch button +client/styles.css - Dependency onboarding progress/step styling +``` + --- 🚨 **END OF FILE - ENSURE YOU READ EVERYTHING ABOVE** 🚨 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d36dc55b..1165aa5f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,16 +5,12 @@ 1. Create a branch from `origin/main`. 2. Make focused changes (small PRs preferred). 3. Run tests for touched areas (at minimum `npm run test:unit`). -4. Run public-release safety checks: - - `npm run audit:public-release` - - `npm run audit:public-release:history` (requires `gitleaks`) -5. Push branch and open a PR. +4. Push branch and open a PR. ```bash git fetch origin git checkout -b feat/my-change origin/main npm run test:unit -npm run audit:public-release git add -A git commit -m "feat: short summary" git push -u origin feat/my-change diff --git a/PLANS/2026-02-02/PHASE4_FULL_UI_CONTROL_REMAINING_WORK.md b/PLANS/2026-02-02/PHASE4_FULL_UI_CONTROL_REMAINING_WORK.md index 53a5ee90..d95469c3 100644 --- a/PLANS/2026-02-02/PHASE4_FULL_UI_CONTROL_REMAINING_WORK.md +++ b/PLANS/2026-02-02/PHASE4_FULL_UI_CONTROL_REMAINING_WORK.md @@ -46,7 +46,7 @@ Target behavior: - be available to Commander typed execution. Work needed: -- Build an exported β€œcommand manifest” from `server/commandRegistry.js` (name, params schema, examples, safety notes). (DONE: explicit `safetyNotes` now included in catalog/capabilities and Settings has a live Command Catalog help view) +- Build an exported β€œcommand manifest” from `server/commandRegistry.js` (name, params schema, examples, safety notes). (PARTIAL: `GET /api/commander/capabilities` exists; remaining: add explicit safety notes + UI help view) - Update voice LLM fallback prompt to include the manifest (or a scoped subset + retrieval). (DONE) - Add `POST /api/commander/execute-text`: (DONE: PR #574) - input: free text @@ -64,9 +64,8 @@ Current state is usable, but not β€œone-screen batch reviewing”. Work needed: - Make **Diff embed always on by default**, and treat β€œOpen in new tab” as secondary. (DONE: PR #587) -- Add **filter/sort controls inside Review Console** (not just Queue). (DONE: this branch) - - filters: tier/risk/unreviewed/blocked/claim - - sort modes: queue order, risk+verify, verify desc, updated desc +- Add **filter/sort controls inside Review Console** (not just Queue): + - by tier/risk/verifyMinutes/blocked/unreviewed - quick β€œnext unreviewed tier 3” navigation. - Reduce vertical waste: - compact headers, tighten paddings, avoid tall meta blocks. @@ -95,7 +94,7 @@ Work needed: - Add a recovery policy: - default: only show sessions not explicitly closed - optionally show β€œclosed (archived)” in a collapsible section. -- DONE: Settings now includes per-workspace β€œPrune old recoverables” (older-than-days) wired to recovery prune API. +- Add UI affordance: β€œClear all recoverable sessions (older than N days)” with confirmation. --- @@ -137,9 +136,9 @@ Work needed: - enumerate all hard-coded colors and convert to CSS variables - document which variables a skin is allowed to override (`--skin-*` targets). - Add more skins with tuned neutral surface targets: - - blue (done), plus emerald/purple/amber (done) and a high-contrast option. (DONE: this branch) + - blue (done), plus at least 2 more β€œbeautiful” schemes (e.g. emerald + violet) and a high-contrast option. - Add a β€œtheme gallery” in Settings: - - preview swatches, quick toggle, and a short explanation of intensity. (DONE: this branch) + - preview swatches, quick toggle, and a short explanation of intensity. --- @@ -161,5 +160,5 @@ Work needed: ## 8) Known UX papercuts (small but important) - Reduce or eliminate multiple β€œβœ•β€ close buttons that do different things without clarity. -- DONE: Queue action bar now wraps by group and keeps controls in-frame on narrower/shorter viewports (no hidden off-screen buttons). -- DONE: Settings panel now uses dynamic viewport height + mobile full-width fallback, keeping the full panel scrollable/usable on short viewports. +- Ensure Queue action bar never hides buttons off-screen (wrap/overflow). +- Ensure settings panel always stays usable on short viewports. diff --git a/PLANS/2026-02-02/REMAINING_WORK_TODAY.md b/PLANS/2026-02-02/REMAINING_WORK_TODAY.md index cc72bcaa..04619fdf 100644 --- a/PLANS/2026-02-02/REMAINING_WORK_TODAY.md +++ b/PLANS/2026-02-02/REMAINING_WORK_TODAY.md @@ -13,8 +13,6 @@ Status: - βœ… Skin selector exists (Settings β†’ Skin). - βœ… Blue skin uses requested primary `#0f67fd` and works in light + dark. - βœ… Skin intensity shipped (Settings β†’ Skin intensity; persisted via `ui.skinIntensity`). -- βœ… Additional skins shipped (Purple/Emerald/Amber + High Contrast). -- βœ… Theme gallery shipped (clickable swatches in Settings). - βœ… Color audit exists (`PLANS/2026-01-31/UI_COLOR_AUDIT.md`). - βœ… Started tokenizing a few hard-coded accents (YOLO highlight + warning accents) to respect skins. @@ -23,6 +21,7 @@ Remaining work: - Decide + document which UI surfaces are β€œaccent tinted” vs neutral across skins: - selected rows/tiles, active tabs, focused buttons, modals/overlays - Queue/Tasks/Review Console overlays +- Add 1–2 additional skins as validation of the architecture (e.g. Purple/Emerald) and QA light+dark readability. Primary sources: - `PLANS/2026-01-31/THEMING_SKINS_BLUE_MODE_PLAN.md` @@ -38,14 +37,12 @@ Status: - βœ… Commander execute + capabilities shipped. - βœ… Queue + Review Console core control surface shipped. - βœ… Voice exact-match aliases are auto-generated from command metadata (prevents drift). -- βœ… Selection helper supports repo-aware PR number targeting (`select pr 492 in zoo-game`) via shared command surface. -- βœ… Commander typed freeform parsing is available via `POST /api/commander/execute-text` (rules first, LLM fallback via the same voice pipeline). Remaining work: -- βœ… Provider plugin interface (Claude/Codex/future) shipped via `server/agentProviderService.js`: - - `listSessions`, `resume` plan generation, `searchHistory`, `getTranscript` - - REST surface: `/api/agent-providers/*` - - Plugin wiring: provider service is now available in plugin loader service context +- Provider plugin interface (Claude/Codex/future) to keep the architecture clean: + - `listSessions`, `resume`, `searchHistory`, `getTranscript`, etc. +- Commander β€œtyped freeform β†’ LLM parse β†’ {command, params}” flow (same prompt logic as voice, but typed). +- Expand selection helpers so voice can do β€œselect PR 492 in zoo-game” (repo alias + PR number) without requiring full URL. Primary source: - `PLANS/2026-01-31/FULL_UI_CONTROL_VIA_COMMANDS_GAP_ANALYSIS.md` diff --git a/PLANS/2026-02-02/WINDOWS_DISTRIBUTION_AND_MONETIZATION_PLAN.md b/PLANS/2026-02-02/WINDOWS_DISTRIBUTION_AND_MONETIZATION_PLAN.md index a9b00234..635a57d5 100644 --- a/PLANS/2026-02-02/WINDOWS_DISTRIBUTION_AND_MONETIZATION_PLAN.md +++ b/PLANS/2026-02-02/WINDOWS_DISTRIBUTION_AND_MONETIZATION_PLAN.md @@ -281,17 +281,8 @@ Bundling the public key into a Tauri build: - `scripts/tauri/prepare-backend-resources.js` will copy it into `resources/backend/license-public-key.pem` so the packaged backend can verify signatures offline. ### Phase C β€” Team/Enterprise add-ons (still local) -- [x] RBAC/policy layer -- [x] audit log export + redaction tools - -Shipped in this slice: -- `server/policyService.js` adds role/action policy checks (`viewer`/`operator`/`admin`) with optional header/query role override. -- High-risk API routes now enforce policy checks (`/api/workspaces/remove-worktree`, `/api/git/pull`, `/api/prs/merge`, `/api/process/automations/pr-merge/run`, `/api/license/set`, commander execute routes). -- `server/auditExportService.js` adds redacted audit export across activity + scheduler logs. -- New audit/policy endpoints: - - `GET /api/policy/status` - - `GET /api/audit/status` - - `GET /api/audit/export` (`json|csv`, pro-gated, redaction enabled by default) +- [ ] RBAC/policy layer +- [ ] audit log export + redaction tools --- diff --git a/PLANS/2026-02-05/HISTORY_REWRITE_PRIVACY_EMAILS_PLAN.md b/PLANS/2026-02-05/HISTORY_REWRITE_PRIVACY_EMAILS_PLAN.md index e6cd7e75..7ba1a282 100644 --- a/PLANS/2026-02-05/HISTORY_REWRITE_PRIVACY_EMAILS_PLAN.md +++ b/PLANS/2026-02-05/HISTORY_REWRITE_PRIVACY_EMAILS_PLAN.md @@ -48,13 +48,6 @@ If you don’t need to keep history: Pros: simplest, avoids history leaks entirely. Cons: loses blame/history/PR linkage. -Automated helper for this path: -- `npm run prep:public-snapshot-repo` -- Optional output directory: - - `npm run prep:public-snapshot-repo -- --out-dir /tmp/claude-orchestrator-public` -- Output is a new local git repo with tracked files copied and a single initial commit. -- This does not touch the current repo history. - --- ## Operational plan (history rewrite) @@ -80,69 +73,6 @@ Use one of: - `git filter-repo` (recommended) - BFG Repo-Cleaner (less flexible) -Non-destructive prep helper now available: -- `npm run audit:history-authors` -- Optional outputs: - - `node scripts/audit-history-authors.js --json /tmp/history-authors.json --md /tmp/history-authors.md --mailmap /tmp/history-authors.mailmap` -- This does not rewrite history; it only audits author/committer email usage and generates a private mailmap starter file. -- Tool bootstrap helper: - - `npm run setup:history-rewrite-tools` - - Optional auto-install attempt: - - `npm run setup:history-rewrite-tools -- --apply` - - Supports `--only git-filter-repo` / `--only gitleaks` when you need one specific dependency. -- One-command prep pipeline: - - `npm run prep:history-rewrite:pipeline` - - Optional strict maintenance-window gate in one run: - - `npm run prep:history-rewrite:pipeline -- --strict` - - Optional auto-tool bootstrap attempt: - - `npm run prep:history-rewrite:pipeline -- --apply-tools` - - Optional persisted report artifacts: - - `npm run prep:history-rewrite:pipeline -- --report-dir /tmp/history-rewrite-reports` - - writes: - - `prep-report.json` - - `prep-report.md` - - per-step JSON files (`setup-tools.json`, `prepare-workkit.json`, `readiness-check.json`) - - Pipeline runs: - - dependency bootstrap check - - workkit generation - - readiness preflight check -- Post-rewrite verification helper: - - Strict: `npm run check:history-rewrite-result` - - Advisory: `npm run check:history-rewrite-result:advisory` - - Checks: - - custom author/committer emails are removed (or explicitly allowed) - - blocked history paths are absent across `git log --all --name-only` -- Mailmap finalizer helper: - - `npm run prep:history-rewrite:mailmap-finalize -- --workkit-dir /tmp/history-rewrite-workkit` - - Optional explicit target: - - `npm run prep:history-rewrite:mailmap-finalize -- --workkit-dir /tmp/history-rewrite-workkit --target-email ` - - Replaces `REPLACE_WITH_NOREPLY_EMAIL` placeholders in `mailmap.private.txt` with a real noreply email (default from global git config). -- Guarded execution helper (maintenance window): - - Plan only (safe/default): - - `npm run history-rewrite:execute:plan -- --workkit-dir /tmp/history-rewrite-workkit --clone-dir /path/to/fresh-rewrite-clone` - - Execute rewrite in clone (still no push unless requested): - - `npm run history-rewrite:execute:plan -- --workkit-dir /tmp/history-rewrite-workkit --clone-dir /path/to/fresh-rewrite-clone --execute --confirm I_UNDERSTAND_HISTORY_REWRITE` - - Optional force-push (double-confirmed): - - add `--push --confirm-push PUSH_REWRITTEN_HISTORY` - - Safety gates: - - refuses execution if mailmap has placeholders - - refuses execution if clone is dirty - - runs strict post-rewrite verification before any push (unless explicitly skipped) -- Full private execution prep workkit: - - `npm run prep:history-rewrite` - - Optional custom output directory: - - `node scripts/generate-history-rewrite-workkit.js --out-dir /tmp/history-rewrite-workkit` - - Generated artifacts include: - - `history-authors.json` / `history-authors.md` (audit evidence) - - `mailmap.private.txt` (fill in noreply targets) - - `paths-to-remove.txt` (history removal path list) - - `run-filter-repo.sh` + `history-rewrite-runbook.md` (execution helpers) - - This is still non-destructive; no rewrite commands are executed automatically. -- Rewrite readiness gate: - - Advisory mode: `npm run check:history-rewrite-readiness -- --workkit-dir /tmp/history-rewrite-workkit` - - Strict gate mode: `npm run check:history-rewrite-readiness:strict -- --workkit-dir /tmp/history-rewrite-workkit` - - Strict mode fails fast when required prerequisites are missing (repo identity, clean worktree, filter-repo, gitleaks, workkit files). - ### 3) Rewrite: remove files/directories from all history In a fresh clone: @@ -211,28 +141,9 @@ Also update: ## Checklist for β€œOK to go public” -- [x] History rewritten OR new squashed public repo created -- [x] Secrets scan passes (history) -- [x] No tracked caches/DBs -- [x] Default bind host is loopback; LAN requires auth token -- [x] Docs don’t contain personal paths/usernames - -Status notes (2026-02-06): -- History scan now passes via `npm run audit:public-release:history` (uses `.gitleaksignore` for two known fixture fingerprints from historical test data). -- Public docs path hygiene + tracked-artifact checks are automated by `scripts/public-release-audit.js`. -- Remaining destructive item is intentional: rewrite history (or publish a new squashed repo) to remove historical metadata/artifacts. - -Status notes (2026-02-08): -- Added `scripts/audit-history-authors.js` and `npm run audit:history-authors` so rewrite inputs can be prepared safely before any destructive history operation. -- Added `scripts/generate-history-rewrite-workkit.js` and `npm run prep:history-rewrite` to produce a private rewrite runbook + command kit for a controlled maintenance-window execution. -- Added `scripts/check-history-rewrite-readiness.js` and `npm run check:history-rewrite-readiness` to enforce a non-destructive preflight gate before any rewrite maintenance window. -- Added `scripts/setup-history-rewrite-tools.js` and `npm run setup:history-rewrite-tools` for cross-platform dependency bootstrap guidance (`git-filter-repo`, `gitleaks`). -- Added `scripts/run-history-rewrite-prep.js` and `npm run prep:history-rewrite:pipeline` for one-command non-destructive prep orchestration. -- Added `scripts/verify-history-rewrite-result.js` and `npm run check:history-rewrite-result` for post-rewrite pass/fail verification. -- Added `scripts/finalize-history-rewrite-mailmap.js` and `npm run prep:history-rewrite:mailmap-finalize` to convert placeholder mailmap entries to a concrete noreply mapping. -- Added `scripts/execute-history-rewrite.js` and `npm run history-rewrite:execute:plan` as a guarded maintenance-window rewrite executor (plan by default; explicit confirm required for execution/push). -- Added `scripts/create-public-snapshot-repo.js` and `npm run prep:public-snapshot-repo` to generate the alternative single-commit public snapshot repo path. -- Executed `npm run prep:public-snapshot-repo` to create a local single-commit public snapshot repository (non-destructive path complete). -- Added `scripts/generate-release-readiness-report.js` and `npm run report:release-readiness` to produce an objective readiness summary (public-release audits + remaining-work state + snapshot existence), with optional strict history scan via `--include-history`. -- Added `scripts/verify-public-snapshot-repo.js` and `npm run check:public-snapshot-repo` to validate snapshot integrity (exists, git repo, single commit, package.json, in-snapshot audit pass). -- Hardened `npm run report:release-readiness` with git identity guardrails (effective/global noreply checks) and scoped canonical-history custom-email warnings to strict `--include-history` mode. +- [ ] History rewritten OR new squashed public repo created +- [ ] Secrets scan passes (history) +- [ ] No tracked caches/DBs +- [ ] Default bind host is loopback; LAN requires auth token +- [ ] Docs don’t contain personal paths/usernames + diff --git a/PLANS/2026-02-05/PHASE4_PRODUCTION_READY_REMAINING_WORK.md b/PLANS/2026-02-05/PHASE4_PRODUCTION_READY_REMAINING_WORK.md index c84103f9..21439990 100644 --- a/PLANS/2026-02-05/PHASE4_PRODUCTION_READY_REMAINING_WORK.md +++ b/PLANS/2026-02-05/PHASE4_PRODUCTION_READY_REMAINING_WORK.md @@ -131,7 +131,6 @@ References: - βœ… Windows CI runs unit tests on PRs and pushes; Tauri build remains tag/dispatch-gated (`.github/workflows/windows.yml`). - βœ… Windows UX: hide `gh` console windows + improve `gh` auth diagnostics (PR tooling) (`server/pullRequestService.js`, `server/diagnosticsService.js`). - βœ… Optional desktop auto-updater plumbing added for packaged Tauri builds (check/install commands + env-driven updater config + Windows docs). -- βœ… Cross-platform hardening follow-up shipped (`PLANS/2026-02-06/CROSS_PLATFORM_RISK_AUDIT_2026-02-06.md`): safer process execution, branch/update guards, reveal-path validation, and diagnostics platform-smoke checks. ### B) Review Console defaults + reliability improvements - βœ… Review Console defaults to the diff-dominant `review` preset (`client/app.js`). @@ -162,12 +161,6 @@ References: - βœ… History scanned with `gitleaks` (no secrets found). - βœ… Clear plan exists for removing historical artifacts + rewriting author emails (not executed yet). - βœ… Baseline `SECURITY.md` added. -- βœ… Public-release audit automation added (`npm run audit:public-release`, `npm run audit:public-release:history`) covering tracked artifacts, public-doc path hygiene, loopback/auth defaults, and history secret scan. - -### G) Policy + audit export hardening -- βœ… RBAC/policy layer added (`server/policyService.js`) with role-based action checks (`viewer`/`operator`/`admin`) and command authorization. -- βœ… High-impact routes now policy-guarded (`/api/workspaces/remove-worktree`, `/api/git/pull`, `/api/prs/merge`, `/api/process/automations/pr-merge/run`, `/api/license/set`, commander execute routes). -- βœ… Audit export + redaction tooling added (`server/auditExportService.js`) with JSON/CSV export endpoints and default redaction for emails/tokens/home paths. References: - `PUBLIC_RELEASE_AUDIT_2026-02-05.md` diff --git a/PLANS/2026-02-05/PLUGIN_ARCHITECTURE_AND_PRO_GATING.md b/PLANS/2026-02-05/PLUGIN_ARCHITECTURE_AND_PRO_GATING.md index 2d95ec33..9e66ea40 100644 --- a/PLANS/2026-02-05/PLUGIN_ARCHITECTURE_AND_PRO_GATING.md +++ b/PLANS/2026-02-05/PLUGIN_ARCHITECTURE_AND_PRO_GATING.md @@ -136,9 +136,10 @@ Low refactor (plugin scripts + server registry): - higher long-term maintenance risk Medium refactor (panel registry + modular UI): -- 3-6 implementation slices depending on how much UI is touched +- 1–2 weeks depending on how much UI is touched - pays down complexity and enables real plugins High refactor (full framework migration): -- 8+ implementation slices +- 2–6+ weeks - best long-term, but not necessary to start selling + diff --git a/PLANS/2026-02-06/CROSS_PLATFORM_RISK_AUDIT_2026-02-06.md b/PLANS/2026-02-06/CROSS_PLATFORM_RISK_AUDIT_2026-02-06.md index 54c06449..c4232b1e 100644 --- a/PLANS/2026-02-06/CROSS_PLATFORM_RISK_AUDIT_2026-02-06.md +++ b/PLANS/2026-02-06/CROSS_PLATFORM_RISK_AUDIT_2026-02-06.md @@ -152,21 +152,21 @@ Mitigation: --- -## Actionable hardening tasks (status) - -- [x] Remove remaining shell interpolation risk in process-limit checks: - - `server/sessionManager.js` now uses `execFile('pgrep', ['-P', pid])` instead of shell `exec(...)`. -- [x] Harden branch/update safety: - - `server/gitUpdateService.js` now rejects detached/sentinel/invalid branch names before pull/update checks. -- [x] Harden `reveal-in-explorer` path handling: - - `server/index.js` now resolves/stats/exists-checks target paths before launching explorer/file manager. -- [x] Add a β€œplatform smoke” diagnostics section: - - `server/diagnosticsService.js` now returns `platformSmoke` checks for shell/git/gh/gh-auth. - - `client/app.js` diagnostics panel now renders these checks. -- [x] Improve child-process spawn consistency: - - `server/diffViewerService.js` and `server/testOrchestrationService.js` now pass `windowsHide: true`. - - `server/diffViewerService.js` validates `cwd` before spawning child processes. +## Actionable hardening tasks (next) + +1) Replace remaining `exec()` string invocations that include interpolation: + - `server/gitUpdateService.js` (branch name interpolation) + - `server/index.js` `reveal-in-explorer` handler (quoting) + +2) Add a simple β€œplatform smoke” section to Diagnostics: + - confirm shell tool exists (`bash` on linux, `powershell.exe` on windows) + - confirm `git` and `gh` exist and `gh auth status` is OK + +3) Ensure every process spawn uses: + - `windowsHide: true` on Windows + - `cwd` validation (avoid `spawn /bin/sh ENOENT` style errors) References: - `WINDOWS_BUILD_GUIDE.md` - `WINDOWS_QUICK_START.md` + diff --git a/PLANS/2026-02-06/SELLABLE_WINDOWS_RELEASE_PLAYBOOK.md b/PLANS/2026-02-06/SELLABLE_WINDOWS_RELEASE_PLAYBOOK.md index 5bff15f4..471f39ed 100644 --- a/PLANS/2026-02-06/SELLABLE_WINDOWS_RELEASE_PLAYBOOK.md +++ b/PLANS/2026-02-06/SELLABLE_WINDOWS_RELEASE_PLAYBOOK.md @@ -121,9 +121,6 @@ Expected: ### Privacy - Do not bundle user data into installers - Ensure `.env`, `user-settings.json`, `sessions/`, `diff-viewer/cache/`, `test-results/` are ignored and not shipped -- Run: - - `npm run audit:public-release` - - `npm run audit:public-release:history` ### Security hygiene - Avoid shell interpolation where possible (prefer `execFile/spawn` with args) @@ -152,3 +149,4 @@ Good β€œPro” candidates: References: - `PLANS/2026-02-05/PUBLISHING_AND_MONETIZATION_OPTIONS.md` - `PLANS/2026-02-05/PLUGIN_ARCHITECTURE_AND_PRO_GATING.md` + diff --git a/PUBLIC_RELEASE_AUDIT_2026-02-04.md b/PUBLIC_RELEASE_AUDIT_2026-02-04.md index 293b26cb..72e6d0e1 100644 --- a/PUBLIC_RELEASE_AUDIT_2026-02-04.md +++ b/PUBLIC_RELEASE_AUDIT_2026-02-04.md @@ -75,7 +75,7 @@ Normal PR fix: ### Medium (privacy / public-facing cleanliness) 4) **Absolute user paths and usernames in docs** -- Many tracked docs include `/home//...`, `/home//...`, and at least one Windows path `C:\\Users\\\\...`. +- Many tracked docs include `/home/ab/...`, `/home/anrokx/...`, and at least one Windows path `C:\\Users\\AB\\...`. - These are not β€œsecrets”, but they are **personal identifiers** and confusing for new users. Normal PR fix: diff --git a/SECURITY.md b/SECURITY.md index b13ab2c2..55fc2a11 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -32,11 +32,3 @@ If you deploy this tool in a shared environment, treat it like local admin tooli - run on trusted machines only - keep `AUTH_TOKEN` enabled for any non-loopback binding -## Public Release Audit Commands - -Before publishing a release branch publicly: - -- `npm run audit:public-release` -- `npm run audit:public-release:history` - -These checks verify tracked-artifact hygiene, docs path hygiene, bind-host/auth defaults, and history secret scanning. diff --git a/client/app.js b/client/app.js index c9936ba9..6859589c 100644 --- a/client/app.js +++ b/client/app.js @@ -69,27 +69,13 @@ class ClaudeOrchestrator { this.workspaceTypes = {}; this.frameworks = {}; this.workspaceHierarchy = {}; - this.projectTypeTaxonomy = null; - this.projectTypeTaxonomyLoadedAt = 0; this.cascadedConfigs = {}; // Fully merged configs (Global β†’ Category β†’ Framework β†’ Project) this.worktreeConfigs = new Map(); // Worktree-specific configs (sessionId β†’ config) this.worktreeTags = new Map(); // Worktree path β†’ tags (e.g., readyForReview) this.taskRecords = new Map(); // taskId β†’ record (tier/risk/pFail/promptRef) - this.commandCatalogCache = []; // Cached /api/commands/catalog payload for local filtering - this.intentHaikuBySession = new Map(); // sessionId -> { summary, source, generatedAt } - this.intentHaikuTimers = new Map(); // sessionId -> timeout id - this.intentHaikuInFlight = new Set(); // sessionIds currently fetching - this.intentHaikuLastFetchedAt = new Map(); // sessionId -> epoch ms - this.intentHaikuRefreshMs = 15000; - this.intentHaikuInitialRefreshDelayMs = 30000; - this.intentHaikuPostPromptDelayMs = 30000; - this.intentHaikuLongRefreshMs = 3 * 60 * 60 * 1000; - this.intentHaikuPolicyBySession = new Map(); // sessionId -> milestone refresh state - this.sessionVisibilityOverridesByWorkspace = this.loadSessionVisibilityOverrides(); // Launch helpers (ticket/card β†’ worktree β†’ agent β†’ auto-prompt) - this.pendingAutoPrompts = new Map(); // sessionId -> { text, createdAt, sentAt, agentId } - this.pendingAutoPromptFallbackTimers = new Map(); // sessionId -> timeout + this.pendingAutoPrompts = new Map(); // sessionId -> { text, createdAt, sentAt } this.pendingWorktreeLaunches = new Map(); // worktreeId -> { promptText, autoSendPrompt, agentConfig } // Optimistic β€œin-use” tracking: while a worktree is starting (sessions not yet added), treat it as in-use // so Quick Work / Add Worktree won’t recommend it again. @@ -97,21 +83,15 @@ class ClaudeOrchestrator { // Worktree launches that should not auto-start or auto-show terminals when sessions arrive. this.pendingBackgroundWorktrees = new Set(); // worktreeId this.scannedReposCache = { value: null, fetchedAt: 0 }; - this.projectsBoardCache = { value: null, fetchedAt: 0 }; this.worktreeModalKeepOpen = this.loadWorktreeModalKeepOpenPreference(); - this.sidebarProjectShortcutsRenderTimer = null; - this.sidebarProjectShortcutsRenderInFlight = false; // Button registry - all available buttons with their implementations this.buttonRegistry = this.initButtonRegistry(); - this.uiVisibility = this.getUiVisibilityConfig(); - // Review Console: when terminals are embedded into the console, we temporarily // move existing terminal wrappers into the modal and restore them on close. this.reviewConsoleDockedTerminals = new Map(); // sessionId -> { wrapper, parent, nextSibling } this.reviewConsoleDockedWorktreePath = null; - this.reviewConsoleHotkeysCleanup = null; // When Review Console is opened from Queue, capture the current filtered PR list so you can // navigate Prev/Next without reopening the Queue overlay. this.reviewConsoleNav = null; // { source, createdAtMs, items: [{ id, kind, title, url, ... }], index } @@ -127,474 +107,6 @@ class ClaudeOrchestrator { } } - isEditableEventTarget(target) { - const el = target && target.nodeType === 1 ? target : null; - if (!el) return false; - return !!el.closest('input, textarea, select, [contenteditable="true"], [contenteditable=""]'); - } - - isAgentSession(sessionId) { - const sid = String(sessionId || '').trim(); - if (!sid) return false; - const type = String(this.sessions.get(sid)?.type || '').trim().toLowerCase(); - return type === 'claude' || type === 'codex' || /-(claude|codex)$/.test(sid); - } - - isMainlineBranch(branch) { - const raw = String(branch || '').trim().toLowerCase(); - if (!raw) return true; - const cleaned = raw - .replace(/^refs\/heads\//, '') - .replace(/^origin\//, '') - .replace(/^remotes\/origin\//, ''); - return cleaned === 'main' || cleaned === 'master' || cleaned === 'trunk' || cleaned === 'default'; - } - - getDefaultVisibilityConfig() { - return { - processBanner: false, - header: { - dashboard: true, - newProject: false, - history: false, - prs: false, - queue: true, - chats: false, - commands: false, - reviewRoute: false, - activity: false, - diff: false, - workflowMode: false, - workflowBackground: false, - tierFilters: false, - focusTier2: false, - focusSwap: false, - tasks: false, - ports: false, - commander: true, - recommendations: false, - notifications: true, - settings: true, - connectionStatus: true - }, - sidebar: { - viewPresets: false, - tierFilters: true, - activeFilter: true, - refreshBranch: false, - readyForReview: false, - sessionVisibilityToggles: false, - deleteWorktree: true - }, - terminal: { - intentHints: false, - branchRefresh: false, - closeProcess: false, - removeWorktree: true, - reviewConsole: true, - showOnlyWorktree: true, - startAgentOptions: true, - startClaudeWithSettings: false, - createNewProject: false, - refreshTerminal: false, - interrupt: false, - assignCodeReview: true, - buildProductionZip: false, - viewBranchOnGithub: false, - viewBranchDiff: true, - viewPrOnGithub: true, - advancedDiff: false, - advancedBranchDiff: false, - startServerDev: false, - forceKill: true, - launchSettings: false, - startServer: true - }, - dashboard: { - processBanner: false, - processSection: false, - statusCard: false, - telemetryCard: false, - polecatsCard: false, - discordCard: false, - projectsCard: false, - adviceCard: false, - readinessCard: false, - suggestions: false, - workspacesActive: true, - workspacesAll: true, - reviewSection: true, - quickLinks: true, - runningServices: true, - createSection: true - }, - commander: { - cmdMode: false, - startStop: false, - startClaude: false, - advice: false, - sessions: true, - modeSelect: false - } - }; - } - - getUiVisibilityConfig() { - const defaults = this.getDefaultVisibilityConfig(); - const next = this.userSettings?.global?.ui?.visibility || {}; - return { - ...defaults, - ...next, - header: { ...defaults.header, ...(next.header || {}) }, - sidebar: { ...defaults.sidebar, ...(next.sidebar || {}) }, - terminal: { ...defaults.terminal, ...(next.terminal || {}) }, - dashboard: { ...defaults.dashboard, ...(next.dashboard || {}) }, - commander: { ...defaults.commander, ...(next.commander || {}) } - }; - } - - getVisibilityValue(config, key) { - if (!key) return true; - const parts = String(key || '').split('.').filter(Boolean); - let current = config; - for (const part of parts) { - if (!current || typeof current !== 'object') return undefined; - current = current[part]; - } - return current; - } - - applyUiVisibility() { - this.uiVisibility = this.getUiVisibilityConfig(); - const nodes = document.querySelectorAll('[data-ui-visibility]'); - nodes.forEach((node) => { - const key = node.dataset.uiVisibility || ''; - const value = this.getVisibilityValue(this.uiVisibility, key); - const shouldShow = value !== false; - node.classList.toggle('hidden', !shouldShow); - }); - } - - getSessionVisibilityStorageKey() { - return 'orchestrator-session-visibility-overrides-v1'; - } - - getSessionVisibilityWorkspaceKey(workspaceId = null) { - const raw = String(workspaceId || this.currentWorkspace?.id || '').trim(); - return raw || '__default__'; - } - - loadSessionVisibilityOverrides() { - const byWorkspace = new Map(); - try { - const raw = localStorage.getItem(this.getSessionVisibilityStorageKey()); - const parsed = raw ? JSON.parse(raw) : {}; - if (!parsed || typeof parsed !== 'object') return byWorkspace; - - for (const [workspaceId, rows] of Object.entries(parsed)) { - if (!rows || typeof rows !== 'object') continue; - const map = new Map(); - for (const [sessionId, hidden] of Object.entries(rows)) { - const sid = String(sessionId || '').trim(); - if (!sid) continue; - if (hidden === false) continue; - // Store only hidden overrides to keep payload compact. - if (hidden === true) map.set(sid, false); - } - if (map.size) byWorkspace.set(String(workspaceId || '').trim() || '__default__', map); - } - } catch { - // ignore persisted state corruption - } - return byWorkspace; - } - - saveSessionVisibilityOverrides() { - try { - const payload = {}; - for (const [workspaceId, rows] of this.sessionVisibilityOverridesByWorkspace.entries()) { - if (!(rows instanceof Map) || rows.size === 0) continue; - const out = {}; - for (const [sessionId, visible] of rows.entries()) { - const sid = String(sessionId || '').trim(); - if (!sid) continue; - if (visible === false) out[sid] = true; - } - if (Object.keys(out).length) payload[workspaceId] = out; - } - localStorage.setItem(this.getSessionVisibilityStorageKey(), JSON.stringify(payload)); - } catch { - // ignore storage failures - } - } - - getSessionVisibilityOverridesForWorkspace(workspaceId = null, { create = false } = {}) { - const key = this.getSessionVisibilityWorkspaceKey(workspaceId); - let rows = this.sessionVisibilityOverridesByWorkspace.get(key); - if (!rows && create) { - rows = new Map(); - this.sessionVisibilityOverridesByWorkspace.set(key, rows); - } - return rows || null; - } - - getSessionVisibilityOverride(sessionId, { workspaceId = null } = {}) { - const sid = String(sessionId || '').trim(); - if (!sid) return null; - const rows = this.getSessionVisibilityOverridesForWorkspace(workspaceId); - if (!rows) return null; - if (!rows.has(sid)) return null; - return rows.get(sid); - } - - setSessionVisibilityOverride(sessionId, visible = null, { workspaceId = null } = {}) { - const sid = String(sessionId || '').trim(); - if (!sid) return; - const rows = this.getSessionVisibilityOverridesForWorkspace(workspaceId, { create: true }); - if (!rows) return; - - if (visible === false) rows.set(sid, false); - else rows.delete(sid); - - if (rows.size === 0) { - const key = this.getSessionVisibilityWorkspaceKey(workspaceId); - this.sessionVisibilityOverridesByWorkspace.delete(key); - } - - this.saveSessionVisibilityOverrides(); - } - - clearSessionVisibilityOverride(sessionId, { allWorkspaces = false } = {}) { - const sid = String(sessionId || '').trim(); - if (!sid) return; - if (!allWorkspaces) { - this.setSessionVisibilityOverride(sid, null); - return; - } - - for (const [workspaceId, rows] of this.sessionVisibilityOverridesByWorkspace.entries()) { - if (!(rows instanceof Map)) continue; - rows.delete(sid); - if (rows.size === 0) this.sessionVisibilityOverridesByWorkspace.delete(workspaceId); - } - this.saveSessionVisibilityOverrides(); - } - - isSessionVisibleByWorktreeSelection(sessionId, session = null) { - const sid = String(sessionId || '').trim(); - if (!sid) return false; - const row = session || this.sessions.get(sid) || null; - const backgroundLaunch = !!row?.backgroundLaunch; - const visibleByWorktreeToggle = this.visibleTerminals.has(sid) - || ((this.workflowMode === 'background' || this.workflowMode === 'all') && backgroundLaunch); - if (!visibleByWorktreeToggle) return false; - - const override = this.getSessionVisibilityOverride(sid, { workspaceId: row?.workspace || null }); - if (override === false) return false; - return true; - } - - toggleSessionVisibility(sessionId) { - const sid = String(sessionId || '').trim(); - if (!sid || !this.sessions.has(sid)) return; - const session = this.sessions.get(sid); - const currentlyVisible = this.isSessionVisibleByWorktreeSelection(sid, session); - this.setSessionVisibilityOverride(sid, currentlyVisible ? false : null, { workspaceId: session?.workspace || null }); - this.updateTerminalGrid(); - this.buildSidebar(); - } - - getSessionIntentHaikuFallback(sessionId) { - const sid = String(sessionId || '').trim(); - const session = this.sessions.get(sid); - const status = String(session?.status || '').trim().toLowerCase(); - const branch = String(session?.branch || '').trim(); - const hasBranch = !!branch && branch !== 'unknown'; - if (status === 'waiting') return 'Context is cached; likely waiting for your next prompt.'; - if (status === 'busy') return 'Reading terminal activity to infer intent...'; - if (hasBranch && !this.isMainlineBranch(branch)) { - return `Branch ${branch}. Context is cached from this branch; likely paused between steps.`; - } - if (hasBranch) { - return `Branch ${branch}. Context is cached; likely waiting for your next prompt.`; - } - return 'Context is cached; likely waiting for your next prompt.'; - } - - getSessionIntentHaikuText(sessionId) { - const sid = String(sessionId || '').trim(); - if (!this.isIntentHintEnabled()) return ''; - const row = this.intentHaikuBySession.get(sid); - const summary = String(row?.summary || '').trim(); - return summary || this.getSessionIntentHaikuFallback(sid); - } - - renderSessionIntentHaiku(sessionId, { pending = false, error = false } = {}) { - const sid = String(sessionId || '').trim(); - if (!sid) return; - if (!this.isIntentHintEnabled()) return; - const el = document.getElementById(this.getSessionDomId('intent-haiku', sid)); - if (!el) return; - - const row = this.intentHaikuBySession.get(sid) || null; - const text = this.getSessionIntentHaikuText(sid); - el.textContent = text; - el.dataset.state = error ? 'error' : (pending ? 'loading' : 'ready'); - - const source = String(row?.source || '').trim(); - const generatedAt = String(row?.generatedAt || '').trim(); - const bits = []; - bits.push('Intent hint'); - if (source) bits.push(`source: ${source}`); - if (generatedAt) bits.push(`updated: ${generatedAt}`); - el.title = bits.join(' | '); - } - - getIntentHaikuPolicy(sessionId) { - const sid = String(sessionId || '').trim(); - if (!sid) return null; - let policy = this.intentHaikuPolicyBySession.get(sid); - if (!policy) { - policy = { - postPromptScheduled: false, - lastPrUrl: '', - lastLongRefreshQueuedAt: 0 - }; - this.intentHaikuPolicyBySession.set(sid, policy); - } - return policy; - } - - maybeSchedulePostPromptIntentRefresh(sessionId) { - const sid = String(sessionId || '').trim(); - if (!this.isIntentHintEnabled()) return; - if (!sid || !this.isAgentSession(sid)) return; - const policy = this.getIntentHaikuPolicy(sid); - if (!policy || policy.postPromptScheduled) return; - policy.postPromptScheduled = true; - this.scheduleSessionIntentHaikuRefresh(sid, { delayMs: this.intentHaikuPostPromptDelayMs, force: true }); - } - - maybeSchedulePrIntentRefresh(sessionId, prUrl) { - const sid = String(sessionId || '').trim(); - const pr = String(prUrl || '').trim(); - if (!this.isIntentHintEnabled()) return; - if (!sid || !this.isAgentSession(sid) || !pr) return; - const policy = this.getIntentHaikuPolicy(sid); - if (!policy) return; - if (policy.lastPrUrl === pr) return; - policy.lastPrUrl = pr; - this.scheduleSessionIntentHaikuRefresh(sid, { delayMs: 1200, force: true }); - } - - maybeScheduleLongSessionIntentRefresh(sessionId) { - const sid = String(sessionId || '').trim(); - if (!this.isIntentHintEnabled()) return; - if (!sid || !this.isAgentSession(sid)) return; - const now = Date.now(); - const lastFetch = Number(this.intentHaikuLastFetchedAt.get(sid) || 0); - if (!lastFetch || (now - lastFetch) < this.intentHaikuLongRefreshMs) return; - - const policy = this.getIntentHaikuPolicy(sid); - if (!policy) return; - if ((now - Number(policy.lastLongRefreshQueuedAt || 0)) < 60000) return; - policy.lastLongRefreshQueuedAt = now; - this.scheduleSessionIntentHaikuRefresh(sid, { delayMs: 3000, force: false }); - } - - pruneIntentHaikuState(activeSessionIds = null) { - const active = activeSessionIds instanceof Set - ? activeSessionIds - : new Set(Array.from(this.sessions.keys()).map((sid) => String(sid || '').trim()).filter(Boolean)); - - for (const sid of Array.from(this.intentHaikuBySession.keys())) { - if (active.has(sid)) continue; - this.intentHaikuBySession.delete(sid); - this.intentHaikuLastFetchedAt.delete(sid); - this.intentHaikuInFlight.delete(sid); - this.intentHaikuPolicyBySession.delete(sid); - const timer = this.intentHaikuTimers.get(sid); - if (timer) clearTimeout(timer); - this.intentHaikuTimers.delete(sid); - } - } - - scheduleSessionIntentHaikuRefresh(sessionId, { delayMs = 2400, force = false } = {}) { - const sid = String(sessionId || '').trim(); - if (!this.isIntentHintEnabled()) return; - if (!sid || !this.isAgentSession(sid)) return; - - const existing = this.intentHaikuTimers.get(sid); - if (existing) clearTimeout(existing); - - const timer = setTimeout(() => { - this.intentHaikuTimers.delete(sid); - this.fetchSessionIntentHaiku(sid, { force }).catch(() => {}); - }, Math.max(250, Number(delayMs) || 0)); - this.intentHaikuTimers.set(sid, timer); - } - - refreshIntentHaikusForAgentSessions({ delayMs = 1400, force = false, onlyMissing = false } = {}) { - if (!this.isIntentHintEnabled()) return; - for (const sessionId of this.sessions.keys()) { - if (!this.isAgentSession(sessionId)) continue; - if (onlyMissing && this.intentHaikuBySession.has(sessionId)) { - this.renderSessionIntentHaiku(sessionId, { pending: false, error: false }); - continue; - } - this.scheduleSessionIntentHaikuRefresh(sessionId, { delayMs, force }); - } - } - - async fetchSessionIntentHaiku(sessionId, { force = false } = {}) { - const sid = String(sessionId || '').trim(); - if (!this.isIntentHintEnabled()) return null; - if (!sid || !this.isAgentSession(sid)) return null; - - if (this.intentHaikuInFlight.has(sid)) return null; - - const now = Date.now(); - const last = this.intentHaikuLastFetchedAt.get(sid) || 0; - if (!force && (now - last) < this.intentHaikuRefreshMs) return null; - - this.intentHaikuInFlight.add(sid); - this.intentHaikuLastFetchedAt.set(sid, now); - this.renderSessionIntentHaiku(sid, { pending: true, error: false }); - - try { - const response = await fetch('/api/sessions/intent-haiku', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ sessionId: sid, force: !!force }) - }); - const payload = await response.json().catch(() => ({})); - if (!response.ok || payload?.ok === false) { - throw new Error(String(payload?.error || payload?.message || `Intent hint failed (${response.status})`)); - } - - const summary = String(payload?.summary || '').trim() || this.getSessionIntentHaikuFallback(sid); - this.intentHaikuBySession.set(sid, { - summary, - source: String(payload?.source || '').trim() || 'heuristic', - generatedAt: String(payload?.generatedAt || '').trim() || new Date().toISOString() - }); - this.renderSessionIntentHaiku(sid, { pending: false, error: false }); - return summary; - } catch (error) { - if (!this.intentHaikuBySession.has(sid)) { - this.intentHaikuBySession.set(sid, { - summary: this.getSessionIntentHaikuFallback(sid), - source: 'fallback', - generatedAt: new Date().toISOString() - }); - } - this.renderSessionIntentHaiku(sid, { pending: false, error: true }); - return null; - } finally { - this.intentHaikuInFlight.delete(sid); - } - } - hashStringToBase36(value) { const s = String(value ?? ''); let h = 5381; @@ -676,49 +188,10 @@ class ClaudeOrchestrator { this.syncSidebarBackdrop(); } - getSidebarDesktopCollapsedPref() { - const fromServer = this.userSettings?.global?.ui?.sidebar?.desktopCollapsed; - if (typeof fromServer === 'boolean') return fromServer; - try { - return localStorage.getItem('sidebar-desktop-collapsed') === 'true'; - } catch { - return false; - } - } - - setSidebarDesktopCollapsed(collapsed) { - const next = !!collapsed; - document.body.classList.toggle('sidebar-collapsed', next); - try { - localStorage.setItem('sidebar-desktop-collapsed', next ? 'true' : 'false'); - } catch { - // ignore - } - try { - this.updateGlobalUserSetting('ui.sidebar.desktopCollapsed', next); - } catch { - // ignore - } - } - - applySidebarDesktopCollapsedFromPrefs() { - try { - document.body.classList.toggle('sidebar-collapsed', !!this.getSidebarDesktopCollapsedPref()); - } catch { - // ignore - } - } - toggleSidebar() { - if (this.isMobileLayout()) { - const isOpen = document.body.classList.contains('sidebar-open'); - if (isOpen) this.closeSidebar(); - else this.openSidebar(); - return; - } - - const collapsed = document.body.classList.contains('sidebar-collapsed'); - this.setSidebarDesktopCollapsed(!collapsed); + const isOpen = document.body.classList.contains('sidebar-open'); + if (isOpen) this.closeSidebar(); + else this.openSidebar(); } loadWorktreeModalKeepOpenPreference() { @@ -767,38 +240,6 @@ class ClaudeOrchestrator { return String(p || '').replace(/\\/g, '/').replace(/\/+$/, '').trim(); } - async ensureProjectTypeTaxonomy({ force = false } = {}) { - if (!force && this.projectTypeTaxonomy && Array.isArray(this.projectTypeTaxonomy.categories)) { - return this.projectTypeTaxonomy; - } - - try { - const response = await fetch('/api/project-types'); - const payload = await response.json().catch(() => ({})); - if (!response.ok) { - throw new Error(String(payload?.error || `HTTP ${response.status}`)); - } - - const categories = Array.isArray(payload?.categories) ? payload.categories : []; - const frameworks = Array.isArray(payload?.frameworks) ? payload.frameworks : []; - const templates = Array.isArray(payload?.templates) ? payload.templates : []; - - this.projectTypeTaxonomy = { - version: Number(payload?.version || 1), - gitHubRoot: String(payload?.gitHubRoot || '').trim(), - categories, - frameworks, - templates, - meta: (payload?.meta && typeof payload.meta === 'object') ? payload.meta : {} - }; - this.projectTypeTaxonomyLoadedAt = Date.now(); - return this.projectTypeTaxonomy; - } catch (error) { - console.warn('Failed to load project-type taxonomy:', error); - return this.projectTypeTaxonomy; - } - } - reserveWorktree(repoPath, worktreeId, { ttlMs } = {}) { const repo = this.normalizeWorktreePath(repoPath); const id = String(worktreeId || '').trim(); @@ -850,7 +291,6 @@ class ClaudeOrchestrator { try { // Initialize managers this.terminalManager = new TerminalManager(this); - this.terminalManager.autosuggestEnabled = this.settings.autoSuggestions !== false; this.notificationManager = new NotificationManager(this); this.agentModalManager = new AgentModalManager(this); @@ -876,8 +316,8 @@ class ClaudeOrchestrator { // Initialize Greenfield wizard for new project creation if (typeof GreenfieldWizard !== 'undefined') { this.greenfieldWizard = new GreenfieldWizard(this); - document.getElementById('greenfield-btn')?.addEventListener('click', async () => { - await this.openGreenfieldWizard(); + document.getElementById('greenfield-btn')?.addEventListener('click', () => { + this.greenfieldWizard.show(); }); console.log('Greenfield wizard initialized'); } @@ -897,21 +337,12 @@ class ClaudeOrchestrator { }); // Queue / Review inbox panel (process tasks) - document.getElementById('queue-btn')?.addEventListener('click', (event) => { - if (event?.shiftKey) { - this.openReviewInbox({ quick: true }); - return; - } - this.openReviewInbox(); + document.getElementById('queue-btn')?.addEventListener('click', () => { + this.showQueuePanel(); }); document.getElementById('project-chats-btn')?.addEventListener('click', () => { this.showProjectChatsShell(); }); - document.getElementById('command-palette-btn')?.addEventListener('click', () => { - this.showCommandPalette().catch((error) => { - this.showToast?.(`Failed to open command palette: ${String(error?.message || error)}`, 'error'); - }); - }); document.getElementById('review-route-btn')?.addEventListener('click', () => { this.openReviewRoute(); }); @@ -993,15 +424,13 @@ class ClaudeOrchestrator { this.installAuthFetchShim(); // Connect to server - await this.connectToServer(); - await this.ensureProjectTypeTaxonomy(); + await this.connectToServer(); // Hook panels that depend on socket events this.activityFeedPanel?.onSocketConnected?.(this.socket); // Load user settings from server await this.loadUserSettings(); - this.applySidebarDesktopCollapsedFromPrefs(); this.refreshLicenseStatus?.().catch(() => {}); this.syncTerminalFiltersFromUserSettings(); this.syncWorkflowModeFromUserSettings(); @@ -1112,10 +541,9 @@ class ClaudeOrchestrator { this.updateServerStatus(sessionId, data); } - // Detect GitHub URLs in agent sessions - if (/-claude$|-codex$/.test(String(sessionId || ''))) { + // Detect GitHub URLs in Claude sessions + if (sessionId.includes('-claude')) { this.detectGitHubLinks(sessionId, data); - this.maybeScheduleLongSessionIntentRefresh(sessionId); } // Fast-path branch refresh when git reports a branch change in output. @@ -1134,10 +562,6 @@ class ClaudeOrchestrator { } }); - this.socket.on('autosuggest-response', ({ sessionId, suggestion, prefix }) => { - this.terminalManager.handleAutosuggestResponse(sessionId, suggestion, prefix); - }); - this.socket.on('status-update', ({ sessionId, status }) => { this.updateSessionStatus(sessionId, status); this.maybeAutoSendPrompt(sessionId, status); @@ -1173,7 +597,44 @@ class ClaudeOrchestrator { } catch { // ignore } - this.removeSessionFromClientState(sessionId, { rebuildUi: true }); + // Remove session from local state + this.sessions.delete(sessionId); + this.visibleTerminals.delete(sessionId); + + // Remove terminal wrapper from UI (scope to the active grid to avoid cross-tab collisions) + const grid = this.getTerminalGrid(); + const wrapperId = this.getSessionDomId('wrapper', sessionId); + let wrapper = document.getElementById(wrapperId); + if (wrapper && grid && !grid.contains(wrapper)) wrapper = null; + if (wrapper) { + console.log(`Removing terminal wrapper from DOM: ${sessionId}`); + wrapper.remove(); + } else { + // Fallback for older DOM shapes + const terminalId = this.getSessionDomId('terminal', sessionId); + let terminalElement = document.getElementById(terminalId); + if (terminalElement && grid && !grid.contains(terminalElement)) terminalElement = null; + if (terminalElement) { + console.log(`Removing terminal element from DOM: ${sessionId}`); + terminalElement.remove(); + } + } + + // Remove from terminal manager + if (this.terminalManager) { + this.terminalManager.destroyTerminal(sessionId); + } + + // Rebuild sidebar to reflect changes + this.buildSidebar(); + this.updateTerminalGrid(); + + // Reflow the grid after removing terminal + if (this.terminalManager && this.terminalManager.fitAllTerminals) { + setTimeout(() => { + this.terminalManager.fitAllTerminals(); + }, 100); + } }); // ============ COMMANDER UI CONTROL ============ @@ -1332,8 +793,7 @@ class ClaudeOrchestrator { this.pendingAutoPrompts.set(agentSessionId, { text: String(pending.promptText || ''), createdAt: Date.now(), - sentAt: null, - agentId: String(pending?.agentConfig?.agentId || 'claude').trim().toLowerCase() + sentAt: null }); } } @@ -1342,7 +802,6 @@ class ClaudeOrchestrator { this.socket.on('claude-started', ({ sessionId }) => { // Hide + persist dismissal so it doesn't resurrect on refresh/worktree-add this.hideStartupUI(sessionId); - this.scheduleAutoPromptFallback(sessionId, 'claude'); // Enable the start button now that Claude has started const startBtn = document.getElementById(`claude-start-btn-${sessionId}`); @@ -1353,9 +812,8 @@ class ClaudeOrchestrator { // Agent-agnostic equivalent (Codex/OpenCode/etc). Startup UI only exists on -claude terminals, // but hiding is safe and prevents resurrection when agent is started via recovery/automation. - this.socket.on('agent-started', ({ sessionId, config }) => { + this.socket.on('agent-started', ({ sessionId }) => { this.hideStartupUI(sessionId); - this.scheduleAutoPromptFallback(sessionId, config?.agentId); }); this.socket.on('claude-update-required', (updateInfo) => { @@ -1487,11 +945,6 @@ class ClaudeOrchestrator { // Pre-fetch worktree-specific configs for all terminals await this.prefetchWorktreeConfigs(workspace, sessions); - // CRITICAL: Set lastSessionsWorkspaceId BEFORE handleInitialSessions so that - // it treats this as a same-workspace refresh and preserves the worktree - // visibility state that was just restored from the tab (fix #786). - this.lastSessionsWorkspaceId = workspace.id; - this.handleInitialSessions(sessions); // Update workspace switcher @@ -1703,22 +1156,6 @@ class ClaudeOrchestrator { 'tasks-theme-select': null, 'tasks-launch-global-prefix': null, 'tasks-launch-include-title': null, - 'review-inbox-mode': null, - 'review-inbox-tiers': null, - 'review-inbox-pr-only': null, - 'review-inbox-unreviewed': null, - 'review-inbox-prioritize-active': null, - 'review-inbox-auto-console': null, - 'review-inbox-auto-advance': null, - 'review-inbox-project': null, - 'quick-review-mode': null, - 'quick-review-tiers': null, - 'quick-review-pr-only': null, - 'quick-review-unreviewed': null, - 'quick-review-prioritize-active': null, - 'quick-review-auto-console': null, - 'quick-review-auto-advance': null, - 'quick-review-project': null, 'trello-me-username': null, 'identity-claim-name': null, 'identity-save': null, @@ -1751,24 +1188,6 @@ class ClaudeOrchestrator { // Sidebar worktree clicks - use toggle instead of show if (elements['worktree-list']) { elements['worktree-list'].addEventListener('click', (e) => { - const shortcutsToggle = e.target.closest('[data-sidebar-project-shortcuts-toggle]'); - if (shortcutsToggle) { - e.preventDefault(); - e.stopPropagation(); - this.setSidebarProjectShortcutsCollapsed(!this.getSidebarProjectShortcutsCollapsed()); - return; - } - - const shortcutBtn = e.target.closest('[data-sidebar-project-shortcut]'); - if (shortcutBtn) { - e.preventDefault(); - e.stopPropagation(); - const key = String(shortcutBtn.getAttribute('data-sidebar-project-shortcut') || '').trim(); - if (key) this.startProjectWorktreeFromBoardKey(key); - if (this.isMobileLayout()) this.closeSidebar(); - return; - } - // Check if click was on ready-for-review toggle const readyBtn = e.target.closest('.ready-review-btn'); if (readyBtn) { @@ -1796,18 +1215,6 @@ class ClaudeOrchestrator { return; } - const visibilityBtn = e.target.closest('.worktree-session-visibility-btn'); - if (visibilityBtn) { - e.preventDefault(); - e.stopPropagation(); - const enc = String(visibilityBtn.dataset.sessionVisibilitySession || '').trim(); - if (!enc) return; - let sessionId = enc; - try { sessionId = decodeURIComponent(enc); } catch { sessionId = enc; } - this.toggleSessionVisibility(sessionId); - return; - } - // Check if click was on delete button if (e.target.closest('.delete-worktree-btn')) { return; // Let the button's onclick handler deal with it @@ -1865,15 +1272,6 @@ class ClaudeOrchestrator { }); } - const projectsBoardBtn = document.getElementById('projects-board-btn'); - if (projectsBoardBtn) { - projectsBoardBtn.addEventListener('click', () => { - try { - this.projectsBoardUI?.show?.(); - } catch {} - }); - } - document.getElementById('view-all').addEventListener('click', () => { this.setViewMode('all'); if (this.isMobileLayout()) this.closeSidebar(); @@ -1981,19 +1379,6 @@ class ClaudeOrchestrator { this.settings.autoScroll = e.target.checked; this.saveSettings(); }); - - document.getElementById('auto-suggestions').addEventListener('change', (e) => { - this.settings.autoSuggestions = e.target.checked; - this.saveSettings(); - if (this.terminalManager) { - this.terminalManager.autosuggestEnabled = e.target.checked; - if (!e.target.checked) { - for (const sessionId of this.terminalManager.terminals.keys()) { - this.terminalManager.clearSuggestion(sessionId); - } - } - } - }); document.getElementById('theme-select').addEventListener('change', (e) => { this.settings.theme = e.target.value; @@ -2003,30 +1388,16 @@ class ClaudeOrchestrator { this.updateGlobalUserSetting('ui.theme', e.target.value); }); - const applySkinSelection = (rawSkin, { persist = true } = {}) => { - const candidate = String(rawSkin || '').trim().toLowerCase(); - const skin = this.getKnownSkins().includes(candidate) ? candidate : 'default'; - this.settings.skin = skin; - this.saveSettings(); - this.applyTheme(); - this.syncSkinGallerySelection(); - if (persist) this.updateGlobalUserSetting('ui.skin', skin); - }; - const skinSelect = document.getElementById('skin-select'); if (skinSelect) { skinSelect.addEventListener('change', (e) => { - applySkinSelection(e.target.value, { persist: true }); + this.settings.skin = e.target.value; + this.saveSettings(); + this.applyTheme(); + this.updateGlobalUserSetting('ui.skin', e.target.value); }); } - document.querySelectorAll('[data-skin-swatch]').forEach((btn) => { - btn.addEventListener('click', () => { - const next = String(btn?.dataset?.skinSwatch || '').trim().toLowerCase(); - applySkinSelection(next, { persist: true }); - }); - }); - const skinIntensityRange = document.getElementById('skin-intensity-range'); const skinIntensityValue = document.getElementById('skin-intensity-value'); if (skinIntensityRange) { @@ -2048,6 +1419,7 @@ class ClaudeOrchestrator { // Settings UI helpers: search + section jump so the panel doesn’t feel like an endless scroll. this.setupSettingsPanelNavigation(); this.setupDiagnosticsPanel(); + this.setupDependencySetupWizard(); const tasksThemeSelect = document.getElementById('tasks-theme-select'); if (tasksThemeSelect) { @@ -2259,14 +1631,6 @@ class ClaudeOrchestrator { this.updateGlobalUserSetting('sessionRecovery.skipPermissions', e.target.checked); }); } - const recoveryPruneBtn = document.getElementById('recovery-prune-old'); - if (recoveryPruneBtn) { - recoveryPruneBtn.addEventListener('click', () => { - this.pruneOldRecoverablesFromSettings().catch((error) => { - this.showToast?.(String(error?.message || error), 'error'); - }); - }); - } // Template management buttons document.getElementById('reset-to-defaults').addEventListener('click', () => { @@ -2357,7 +1721,7 @@ class ClaudeOrchestrator { if (reviewConsolePreset) { reviewConsolePreset.addEventListener('change', async (e) => { const v = String(e.target.value || '').trim().toLowerCase(); - const allowed = new Set(['default', 'review', 'throughput', 'deep', 'terminals', 'code', 'custom']); + const allowed = new Set(['default', 'review', 'deep', 'terminals', 'code', 'custom']); const preset = allowed.has(v) ? v : 'review'; await this.updateGlobalUserSetting('ui.reviewConsole.preset', preset); }); @@ -2387,116 +1751,12 @@ class ClaudeOrchestrator { }); } - const reviewInboxMode = document.getElementById('review-inbox-mode'); - if (reviewInboxMode) { - reviewInboxMode.addEventListener('change', async (e) => { - const v = String(e.target.value || '').trim().toLowerCase(); - await this.updateGlobalUserSetting('ui.reviewInbox.mode', v === 'all' ? 'all' : 'mine'); - }); - } - const reviewInboxTiers = document.getElementById('review-inbox-tiers'); - if (reviewInboxTiers) { - reviewInboxTiers.addEventListener('change', async (e) => { - const v = String(e.target.value || '').trim().toLowerCase(); - const allowed = new Set(['t3t4', 't1', 't2', 't3', 't4', 'all', 'none']); - await this.updateGlobalUserSetting('ui.reviewInbox.tiers', allowed.has(v) ? v : 't3t4'); - }); - } - const reviewInboxPrOnly = document.getElementById('review-inbox-pr-only'); - if (reviewInboxPrOnly) { - reviewInboxPrOnly.addEventListener('change', async (e) => { - await this.updateGlobalUserSetting('ui.reviewInbox.kind', e.target.checked ? 'pr' : 'all'); - }); - } - const reviewInboxUnreviewed = document.getElementById('review-inbox-unreviewed'); - if (reviewInboxUnreviewed) { - reviewInboxUnreviewed.addEventListener('change', async (e) => { - await this.updateGlobalUserSetting('ui.reviewInbox.unreviewedOnly', !!e.target.checked); - }); - } - const reviewInboxPrioritize = document.getElementById('review-inbox-prioritize-active'); - if (reviewInboxPrioritize) { - reviewInboxPrioritize.addEventListener('change', async (e) => { - await this.updateGlobalUserSetting('ui.reviewInbox.prioritizeActive', !!e.target.checked); - }); - } - const reviewInboxAutoConsole = document.getElementById('review-inbox-auto-console'); - if (reviewInboxAutoConsole) { - reviewInboxAutoConsole.addEventListener('change', async (e) => { - await this.updateGlobalUserSetting('ui.reviewInbox.autoConsole', !!e.target.checked); - }); - } - const reviewInboxAutoAdvance = document.getElementById('review-inbox-auto-advance'); - if (reviewInboxAutoAdvance) { - reviewInboxAutoAdvance.addEventListener('change', async (e) => { - await this.updateGlobalUserSetting('ui.reviewInbox.autoAdvance', !!e.target.checked); - }); - } - const reviewInboxProject = document.getElementById('review-inbox-project'); - if (reviewInboxProject) { - reviewInboxProject.addEventListener('change', async (e) => { - await this.updateGlobalUserSetting('ui.reviewInbox.project', String(e.target.value || '').trim()); - }); - } - - const quickReviewMode = document.getElementById('quick-review-mode'); - if (quickReviewMode) { - quickReviewMode.addEventListener('change', async (e) => { - const v = String(e.target.value || '').trim().toLowerCase(); - await this.updateGlobalUserSetting('ui.quickReview.mode', v === 'all' ? 'all' : 'mine'); - }); - } - const quickReviewTiers = document.getElementById('quick-review-tiers'); - if (quickReviewTiers) { - quickReviewTiers.addEventListener('change', async (e) => { - const v = String(e.target.value || '').trim().toLowerCase(); - const allowed = new Set(['t3t4', 't1', 't2', 't3', 't4', 'all', 'none']); - await this.updateGlobalUserSetting('ui.quickReview.tiers', allowed.has(v) ? v : 't3t4'); - }); - } - const quickReviewPrOnly = document.getElementById('quick-review-pr-only'); - if (quickReviewPrOnly) { - quickReviewPrOnly.addEventListener('change', async (e) => { - await this.updateGlobalUserSetting('ui.quickReview.kind', e.target.checked ? 'pr' : 'all'); - }); - } - const quickReviewUnreviewed = document.getElementById('quick-review-unreviewed'); - if (quickReviewUnreviewed) { - quickReviewUnreviewed.addEventListener('change', async (e) => { - await this.updateGlobalUserSetting('ui.quickReview.unreviewedOnly', !!e.target.checked); - }); - } - const quickReviewPrioritize = document.getElementById('quick-review-prioritize-active'); - if (quickReviewPrioritize) { - quickReviewPrioritize.addEventListener('change', async (e) => { - await this.updateGlobalUserSetting('ui.quickReview.prioritizeActive', !!e.target.checked); - }); - } - const quickReviewAutoConsole = document.getElementById('quick-review-auto-console'); - if (quickReviewAutoConsole) { - quickReviewAutoConsole.addEventListener('change', async (e) => { - await this.updateGlobalUserSetting('ui.quickReview.autoConsole', !!e.target.checked); - }); - } - const quickReviewAutoAdvance = document.getElementById('quick-review-auto-advance'); - if (quickReviewAutoAdvance) { - quickReviewAutoAdvance.addEventListener('change', async (e) => { - await this.updateGlobalUserSetting('ui.quickReview.autoAdvance', !!e.target.checked); - }); - } - const quickReviewProject = document.getElementById('quick-review-project'); - if (quickReviewProject) { - quickReviewProject.addEventListener('change', async (e) => { - await this.updateGlobalUserSetting('ui.quickReview.project', String(e.target.value || '').trim()); - }); - } - - // Simple mode settings (Projects + Chats shell) - const simpleModeEnabled = document.getElementById('simple-mode-enabled'); - if (simpleModeEnabled) { - simpleModeEnabled.addEventListener('change', async (e) => { - await this.updateGlobalUserSetting('ui.simpleMode.enabled', !!e.target.checked); - this.applySimpleModeConfig(); + // Simple mode settings (Projects + Chats shell) + const simpleModeEnabled = document.getElementById('simple-mode-enabled'); + if (simpleModeEnabled) { + simpleModeEnabled.addEventListener('change', async (e) => { + await this.updateGlobalUserSetting('ui.simpleMode.enabled', !!e.target.checked); + this.applySimpleModeConfig(); }); } const simpleModeStartupOpen = document.getElementById('simple-mode-startup-open'); @@ -2588,14 +1848,6 @@ class ClaudeOrchestrator { }); }); } - const schedulerTemplatePreview = document.getElementById('scheduler-template-preview'); - if (schedulerTemplatePreview) { - schedulerTemplatePreview.addEventListener('click', () => { - this.previewSchedulerTemplateFromSettings().catch((error) => { - this.showToast?.(String(error?.message || error), 'error'); - }); - }); - } const schedulerEnabled = document.getElementById('scheduler-enabled'); if (schedulerEnabled) { schedulerEnabled.addEventListener('change', () => { @@ -2605,61 +1857,6 @@ class ClaudeOrchestrator { }); } - const pagerRefresh = document.getElementById('pager-refresh'); - if (pagerRefresh) { - pagerRefresh.addEventListener('click', () => { - this.refreshPagerStatus().catch((error) => { - this.showToast?.(String(error?.message || error), 'error'); - }); - }); - } - const pagerSaveDefaults = document.getElementById('pager-save-defaults'); - if (pagerSaveDefaults) { - pagerSaveDefaults.addEventListener('click', () => { - this.savePagerDefaultsFromSettings().catch((error) => { - this.showToast?.(String(error?.message || error), 'error'); - }); - }); - } - const pagerStart = document.getElementById('pager-start'); - if (pagerStart) { - pagerStart.addEventListener('click', () => { - this.startPagerFromSettings().catch((error) => { - this.showToast?.(String(error?.message || error), 'error'); - }); - }); - } - const pagerStop = document.getElementById('pager-stop'); - if (pagerStop) { - pagerStop.addEventListener('click', () => { - this.stopPagerFromSettings().catch((error) => { - this.showToast?.(String(error?.message || error), 'error'); - }); - }); - } - const commandCatalogRefresh = document.getElementById('command-catalog-refresh'); - if (commandCatalogRefresh) { - commandCatalogRefresh.addEventListener('click', () => { - this.refreshCommandCatalog().catch((error) => { - this.showToast?.(String(error?.message || error), 'error'); - }); - }); - } - const commandCatalogOpenNewProject = document.getElementById('command-catalog-open-new-project'); - if (commandCatalogOpenNewProject) { - commandCatalogOpenNewProject.addEventListener('click', () => { - this.openGreenfieldWizard().catch((error) => { - this.showToast?.(String(error?.message || error), 'error'); - }); - }); - } - const commandCatalogFilter = document.getElementById('command-catalog-filter'); - if (commandCatalogFilter) { - commandCatalogFilter.addEventListener('input', () => { - this.renderCommandCatalog(this.commandCatalogCache); - }); - } - // License controls const licenseReload = document.getElementById('license-reload'); if (licenseReload) { @@ -2802,7 +1999,7 @@ class ClaudeOrchestrator { // Keyboard: Alt+1/2/3/4 set tier filter; Alt+0 or Alt+A sets All; Alt+N sets None. document.addEventListener('keydown', (e) => { - if (!e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) return; + if (!e.altKey || e.ctrlKey || e.metaKey) return; // Ignore when typing in inputs/selects/contenteditable. const t = e.target; @@ -2859,24 +2056,6 @@ class ClaudeOrchestrator { this.showToast(`Mode: ${mode}`, 'info'); }); - // Keyboard: Ctrl/Cmd+K opens command palette. - document.addEventListener('keydown', (e) => { - const lower = String(e.key || '').toLowerCase(); - const openShortcut = (lower === 'k' && (e.ctrlKey || e.metaKey) && !e.altKey); - if (!openShortcut) return; - - // Ignore when typing in inputs/selects/contenteditable. - const t = e.target; - const tag = String(t?.tagName || '').toLowerCase(); - if (tag === 'input' || tag === 'textarea' || tag === 'select' || t?.isContentEditable) return; - - e.preventDefault(); - e.stopPropagation(); - this.showCommandPalette().catch((error) => { - this.showToast?.(`Failed to open command palette: ${String(error?.message || error)}`, 'error'); - }); - }); - // Keyboard: Alt+P opens the simple Projects + Chats shell. document.addEventListener('keydown', (e) => { if (!e.altKey || e.ctrlKey || e.metaKey) return; @@ -2996,48 +2175,8 @@ class ClaudeOrchestrator { this.updateGlobalUserSetting('ui.workflow.mode', normalized); } - async applyReviewRouteUiDefaults() { - const existing = (this.userSettings?.global?.ui?.reviewConsole && typeof this.userSettings.global.ui.reviewConsole === 'object') - ? this.userSettings.global.ui.reviewConsole - : {}; - const existingSections = (existing.sections && typeof existing.sections === 'object') ? existing.sections : {}; - const existingKinds = (existing.terminalKinds && typeof existing.terminalKinds === 'object') ? existing.terminalKinds : {}; - - const nextCfg = { - ...existing, - preset: 'throughput', - fullscreen: true, - diffEmbed: true, - sections: { - ...existingSections, - terminals: true, - files: false, - commits: false, - diff: true - }, - terminalKinds: { - ...existingKinds, - agent: true, - server: true - } - }; - - await this.updateGlobalUserSetting('ui.reviewConsole', nextCfg); - - try { localStorage.setItem('queue-auto-console', 'true'); } catch {} - try { localStorage.setItem('queue-auto-advance', 'true'); } catch {} - try { - localStorage.setItem('review-console-collapsed-panels', JSON.stringify({ - files: false, - commits: true, - conversation: true - })); - } catch {} - } - - async openReviewRoute() { + openReviewRoute() { this.setWorkflowMode('review'); - await this.applyReviewRouteUiDefaults(); this.queuePanelPreset = { reviewTier: 'all', tierSet: [3, 4], @@ -3053,77 +2192,6 @@ class ClaudeOrchestrator { return this.showQueuePanel(); } - normalizeReviewTierPreset(value) { - const raw = String(value || '').trim().toLowerCase(); - if (!raw || raw === 'default') return { preset: 't3t4', reviewTier: 'all', tierSet: [3, 4] }; - if (raw === 'all') return { preset: 'all', reviewTier: 'all', tierSet: null }; - if (raw === 'none') return { preset: 'none', reviewTier: 'none', tierSet: null }; - if (raw === 't3t4' || raw === 't3+4' || raw === 't3+t4' || raw === 'bg') { - return { preset: 't3t4', reviewTier: 'all', tierSet: [3, 4] }; - } - const m = raw.match(/^t?([1-4])$/); - if (m) { - const tier = Number.parseInt(m[1], 10); - return { preset: `t${tier}`, reviewTier: tier, tierSet: null }; - } - return { preset: 't3t4', reviewTier: 'all', tierSet: [3, 4] }; - } - - getReviewInboxDefaults(variant = 'reviewInbox') { - const base = (this.userSettings?.global?.ui?.reviewInbox && typeof this.userSettings.global.ui.reviewInbox === 'object') - ? this.userSettings.global.ui.reviewInbox - : {}; - const quick = (this.userSettings?.global?.ui?.quickReview && typeof this.userSettings.global.ui.quickReview === 'object') - ? this.userSettings.global.ui.quickReview - : {}; - const raw = variant === 'quickReview' ? { ...base, ...quick } : { ...base }; - const tierInfo = this.normalizeReviewTierPreset(raw.tiers); - const mode = String(raw.mode || '').trim().toLowerCase() === 'all' ? 'all' : 'mine'; - const kind = String(raw.kind || '').trim().toLowerCase() === 'all' ? 'all' : 'pr'; - const project = String(raw.project || '').trim(); - - return { - mode, - kind, - project, - tiers: tierInfo.preset, - reviewTier: tierInfo.reviewTier, - tierSet: tierInfo.tierSet, - unreviewedOnly: raw.unreviewedOnly !== false, - autoConsole: raw.autoConsole === true, - autoAdvance: raw.autoAdvance === true, - prioritizeActive: raw.prioritizeActive !== false - }; - } - - async openReviewInbox({ quick = false, project = '' } = {}) { - this.setWorkflowMode('review'); - const defaults = this.getReviewInboxDefaults(quick ? 'quickReview' : 'reviewInbox'); - const projectFilter = String(project || defaults.project || '').trim(); - this.queuePanelPreset = { - mode: defaults.mode, - reviewTier: defaults.reviewTier, - tierSet: defaults.tierSet, - triageMode: false, - unreviewedOnly: defaults.unreviewedOnly, - blockedOnly: false, - autoOpenDiff: false, - autoConsole: defaults.autoConsole, - autoAdvance: defaults.autoAdvance, - reviewActive: true, - reviewRouteActive: false, - kindFilter: defaults.kind, - projectFilter: projectFilter || '', - prioritizeActive: defaults.prioritizeActive, - quickReview: !!quick - }; - return this.showQueuePanel(); - } - - async openQuickReview({ project = '' } = {}) { - return this.openReviewInbox({ quick: true, project }); - } - syncWorkflowModeFromUserSettings() { const mode = this.userSettings?.global?.ui?.workflow?.mode; const normalized = String(mode || '').trim().toLowerCase(); @@ -3326,12 +2394,6 @@ class ClaudeOrchestrator { } startProcessStatusBanner() { - const visibility = this.getUiVisibilityConfig(); - const showHeader = visibility.processBanner !== false; - const showDashboard = visibility.dashboard?.processBanner !== false; - if (!showHeader && !showDashboard) { - return; - } const renderInto = (banner, status) => { if (!banner) return; if (!banner.dataset.bound) { @@ -3458,7 +2520,7 @@ class ClaudeOrchestrator { computeTier1Busy() { for (const [sessionId, session] of this.sessions) { if (!this.matchesViewMode(sessionId)) continue; - if (!((session?.type === 'claude' || session?.type === 'codex') || /-(claude|codex)$/.test(String(sessionId || '')))) continue; + if (!(session?.type === 'claude' || String(sessionId).includes('-claude'))) continue; const tier = this.getTierForSession(sessionId); if (tier !== 1) continue; @@ -3552,7 +2614,7 @@ class ClaudeOrchestrator { const type = session?.type; if (this.viewMode === 'claude') { - return type === 'claude' || type === 'codex' || /-(claude|codex)$/.test(String(sessionId || '')); + return type === 'claude' || sessionId.includes('-claude'); } if (this.viewMode === 'server') { @@ -3564,7 +2626,11 @@ class ClaudeOrchestrator { isSessionVisibleInCurrentView(sessionId) { const session = this.sessions.get(sessionId); - const visibleByWorktreeToggle = this.isSessionVisibleByWorktreeSelection(sessionId, session); + // Background-launched worktrees intentionally do not auto-show in Review/Focus, + // but they should become visible when the user explicitly switches to Background mode. + const backgroundLaunch = !!session?.backgroundLaunch; + const visibleByWorktreeToggle = this.visibleTerminals.has(sessionId) + || ((this.workflowMode === 'background' || this.workflowMode === 'all') && backgroundLaunch); return visibleByWorktreeToggle && this.matchesViewMode(sessionId) @@ -3631,8 +2697,6 @@ class ClaudeOrchestrator { // Debug: console.log(`Added terminal ${sessionId} to visible set, activity: ${this.sessionActivity.get(sessionId)}`); } - this.pruneIntentHaikuState(new Set(Object.keys(sessionStates))); - this.lastSessionsWorkspaceId = currentWorkspaceId; // Hide loading message FIRST @@ -3646,8 +2710,6 @@ class ClaudeOrchestrator { // Show all visible terminals this.updateTerminalGrid(); - // On load, fill missing intent hints once and rely on milestone refreshes afterward. - this.refreshIntentHaikusForAgentSessions({ delayMs: this.intentHaikuInitialRefreshDelayMs, force: false, onlyMissing: true }); // Check for auto-start after a delay to let terminals initialize setTimeout(() => { @@ -3794,13 +2856,6 @@ class ClaudeOrchestrator { showWhen: 'always', terminalType: 'claude' }, - newProject: { - icon: '✨', - title: 'Create New Project', - action: 'openGreenfieldWizard', - showWhen: 'always', - terminalType: 'both' - }, // Server terminal buttons play: { @@ -3842,32 +2897,6 @@ class ClaudeOrchestrator { }; } - getTerminalVisibilityConfig() { - return this.getUiVisibilityConfig().terminal || {}; - } - - isIntentHintEnabled() { - return this.getTerminalVisibilityConfig().intentHints !== false; - } - - shouldRenderTerminalButton(buttonId) { - const visibility = this.getTerminalVisibilityConfig(); - const map = { - focus: 'showOnlyWorktree', - newProject: 'createNewProject', - interrupt: 'interrupt', - claudeStart: 'startClaudeWithSettings', - claudeModal: 'startAgentOptions', - refresh: 'refreshTerminal', - review: 'assignCodeReview', - build: 'buildProductionZip', - kill: 'forceKill' - }; - const key = map[buttonId]; - if (!key) return true; - return visibility[key] !== false; - } - /** * Get buttons for a session based on cascaded config * @param {string} sessionId @@ -3916,13 +2945,8 @@ class ClaudeOrchestrator { const buttonDefs = cascadedConfig.buttons[terminalType] || {}; const buttons = []; - // Always add focus + create-project quick action first - if (this.shouldRenderTerminalButton('focus')) { - buttons.push(this.renderButton('focus', this.buttonRegistry.focus, sessionId)); - } - if (this.shouldRenderTerminalButton('newProject')) { - buttons.push(this.renderButton('newProject', this.buttonRegistry.newProject, sessionId)); - } + // Always add focus button first + buttons.push(this.renderButton('focus', this.buttonRegistry.focus, sessionId)); // Render configured buttons for (const [buttonId, buttonConfig] of Object.entries(buttonDefs)) { @@ -3931,7 +2955,6 @@ class ClaudeOrchestrator { // Merge config with registry const mergedButton = { ...registryEntry, ...buttonConfig }; - if (!this.shouldRenderTerminalButton(buttonId)) continue; buttons.push(this.renderButton(buttonId, mergedButton, sessionId)); } @@ -3955,7 +2978,6 @@ class ClaudeOrchestrator { showClaudeStartupModal: `window.orchestrator.showClaudeStartupModal('${sessionId}')`, refreshTerminal: `window.orchestrator.refreshTerminal('${sessionId}')`, showCodeReviewDropdown: `window.orchestrator.showCodeReviewDropdown('${sessionId}')`, - openGreenfieldWizard: `window.orchestrator.openGreenfieldWizard()`, playInHytopia: `window.orchestrator.playInHytopia('${sessionId}')`, copyLocalhostUrl: `window.orchestrator.copyLocalhostUrl('${sessionId}')`, openHytopiaWebsite: `window.orchestrator.openHytopiaWebsite()`, @@ -3973,24 +2995,22 @@ class ClaudeOrchestrator { */ getDefaultButtons(terminalType, sessionId = '') { if (terminalType === 'claude') { - const ids = [ - 'focus', - 'newProject', - 'claudeStart', - 'claudeModal', - 'refresh', - 'interrupt', - 'review', - 'build' + return [ + this.renderButton('focus', this.buttonRegistry.focus, sessionId), + this.renderButton('claudeStart', this.buttonRegistry.claudeStart, sessionId), + this.renderButton('claudeModal', this.buttonRegistry.claudeModal, sessionId), + this.renderButton('refresh', this.buttonRegistry.refresh, sessionId), + this.renderButton('interrupt', this.buttonRegistry.interrupt, sessionId), + this.renderButton('review', this.buttonRegistry.review, sessionId), + this.renderButton('build', this.buttonRegistry.build, sessionId) ]; - return ids - .filter((id) => this.shouldRenderTerminalButton(id)) - .map((id) => this.renderButton(id, this.buttonRegistry[id], sessionId)); } else { - const ids = ['focus', 'newProject', 'build', 'interrupt', 'kill']; - return ids - .filter((id) => this.shouldRenderTerminalButton(id)) - .map((id) => this.renderButton(id, this.buttonRegistry[id], sessionId)); + return [ + this.renderButton('focus', this.buttonRegistry.focus, sessionId), + this.renderButton('build', this.buttonRegistry.build, sessionId), + this.renderButton('interrupt', this.buttonRegistry.interrupt, sessionId), + this.renderButton('kill', this.buttonRegistry.kill, sessionId) + ]; } } @@ -4092,8 +3112,6 @@ class ClaudeOrchestrator { */ getServerControlsHTML(sessionId) { const isRunning = this.serverStatuses.get(sessionId) === 'running'; - const visibility = this.getTerminalVisibilityConfig(); - const showLaunchSettings = visibility.launchSettings !== false; // Start with server control (start/stop/launch) let html = ''; @@ -4108,7 +3126,7 @@ class ClaudeOrchestrator { ${this.getDynamicLaunchOptions(sessionId)} - ${showLaunchSettings ? `` : ''} + `; } @@ -4134,8 +3152,8 @@ class ClaudeOrchestrator { getLinkedServerSessionIdForClaude(claudeSessionId) { const sid = String(claudeSessionId || '').trim(); - if (!sid.endsWith('-claude') && !sid.endsWith('-codex')) return null; - const serverSessionId = sid.replace(/-(claude|codex)$/, '-server'); + if (!sid.endsWith('-claude')) return null; + const serverSessionId = sid.replace(/-claude$/, '-server'); if (!this.sessions || !this.sessions.has(serverSessionId)) return null; return serverSessionId; } @@ -4144,19 +3162,14 @@ class ClaudeOrchestrator { const sid = String(serverSessionId || '').trim(); if (!sid.endsWith('-server')) return null; const claudeSessionId = sid.replace(/-server$/, '-claude'); - if (this.sessions && this.sessions.has(claudeSessionId)) return claudeSessionId; - const codexSessionId = sid.replace(/-server$/, '-codex'); - if (this.sessions && this.sessions.has(codexSessionId)) return codexSessionId; - return null; + if (!this.sessions || !this.sessions.has(claudeSessionId)) return null; + return claudeSessionId; } getServerQuickControlsHTMLForClaude(claudeSessionId) { const serverSessionId = this.getLinkedServerSessionIdForClaude(claudeSessionId); if (!serverSessionId) return ''; - const visibility = this.getTerminalVisibilityConfig(); - if (visibility.startServerDev === false) return ''; - const isRunning = this.serverStatuses.get(serverSessionId) === 'running'; if (isRunning) { return ``; @@ -4164,76 +3177,11 @@ class ClaudeOrchestrator { return ``; } - getSessionWorktreeKey(sessionId, session = null) { - const sid = String(sessionId || '').trim(); - const row = session || this.sessions.get(sid) || null; - if (!sid && !row) return ''; - const worktreeId = String(row?.worktreeId || sid.split('-')[0] || '').trim(); - const repositoryName = String(row?.repositoryName || this.extractRepositoryName(sid) || '').trim(); - return repositoryName ? `${repositoryName}-${worktreeId}` : worktreeId; - } - - getSidebarAgentIdForWorktree(worktreeKey) { - const session = this.getSidebarAgentSessionForWorktree(worktreeKey); - if (!session) return null; - const agentId = String(session?.agent || '').trim(); - return agentId || null; - } - - getSidebarAgentSessionForWorktree(worktreeKey) { - const target = String(worktreeKey || '').trim(); - if (!target) return null; - let fallback = null; - for (const [sessionId, session] of this.sessions) { - const type = String(session?.type || '').trim().toLowerCase(); - if (type !== 'claude' && type !== 'codex') continue; - const key = this.getSessionWorktreeKey(sessionId, session); - if (key !== target) continue; - if (type === 'claude') return session; - if (!fallback) fallback = session; - } - return fallback; - } - - getSidebarVisualStatusForWorktree(worktreeKey, status) { - const normalized = String(status || '').trim().toLowerCase() || 'idle'; - if (normalized === 'waiting') { - const agentSession = this.getSidebarAgentSessionForWorktree(worktreeKey); - if (agentSession && !agentSession.hasUserInput) return 'ready-new'; - return 'waiting'; - } - if (normalized !== 'idle') return normalized; - const agentId = this.getSidebarAgentIdForWorktree(worktreeKey); - return agentId ? 'idle' : 'no-agent'; - } - - getStatusTitleForVisualStatus(status) { - const normalized = String(status || '').trim().toLowerCase(); - if (normalized === 'no-agent') return 'no AI running'; - if (normalized === 'ready-new') return 'waiting (new session)'; - if (normalized === 'waiting') return 'waiting for your input'; - if (normalized === 'busy') return 'busy'; - if (normalized === 'idle') return 'idle'; - return normalized || 'unknown'; - } - - getVisualStatusForSession(sessionId, status, session = null) { - const sid = String(sessionId || '').trim(); - const row = session || this.sessions.get(sid) || null; - const raw = String(status || row?.status || '').trim().toLowerCase() || 'idle'; - if (raw !== 'idle' && raw !== 'waiting' && raw !== 'busy') return raw; - - const worktreeKey = this.getSessionWorktreeKey(sid, row); - if (!worktreeKey) return raw; - return this.getSidebarVisualStatusForWorktree(worktreeKey, raw); - } - buildSidebar() { const worktreeList = document.getElementById('worktree-list'); if (!worktreeList) return; const previousScrollTop = worktreeList.scrollTop; - const sidebarVisibility = this.getUiVisibilityConfig().sidebar || {}; // Always ensure filter toggle exists and is updated FIRST this.ensureFilterToggleExists(); @@ -4285,10 +3233,10 @@ class ClaudeOrchestrator { continue; } - // Check if any session in this worktree is visible. - const claudeVisible = !!(worktree.claude && this.isSessionVisibleByWorktreeSelection(worktree.claude.sessionId, worktree.claude)); - const serverVisible = !!(worktree.server && this.isSessionVisibleByWorktreeSelection(worktree.server.sessionId, worktree.server)); - const isVisible = claudeVisible || serverVisible; + // Check if any session in this worktree is visible + const backgroundMode = this.workflowMode === 'background'; + const isVisible = (worktree.claude && (this.visibleTerminals.has(worktree.claude.sessionId) || (backgroundMode && worktree.claude.backgroundLaunch))) || + (worktree.server && (this.visibleTerminals.has(worktree.server.sessionId) || (backgroundMode && worktree.server.backgroundLaunch))); const item = document.createElement('div'); // Only show visibility state, not activity state (activity filtering is handled separately) @@ -4302,18 +3250,11 @@ class ClaudeOrchestrator { // Single-dot sidebar status: prefer the agent (Claude) status const sidebarStatus = worktree.claude?.status || worktree.server?.status || 'idle'; - const sidebarStatusVisual = this.getSidebarVisualStatusForWorktree(worktree.id, sidebarStatus); - const agentSession = this.getSidebarAgentSessionForWorktree(worktree.id); - - const agentId = this.getSidebarAgentIdForWorktree(worktree.id) || worktree.claude?.agent || worktree.server?.agent || null; - const noAgentRunning = sidebarStatus === 'idle' && !agentId; - const freshWaiting = sidebarStatus === 'waiting' && agentSession && !agentSession.hasUserInput; - const statusLabel = noAgentRunning - ? 'idle (no AI running)' - : (freshWaiting ? 'waiting (new session)' : sidebarStatus); + + const agentId = worktree.claude?.agent || worktree.server?.agent || null; const statusTitleParts = [ - `Status: ${statusLabel}`, - agentId ? `Agent: ${agentId}` : (noAgentRunning ? 'Agent: none' : null) + `Status: ${sidebarStatus}`, + agentId ? `Agent: ${agentId}` : null ].filter(Boolean); const statusTitle = statusTitleParts.join(' β€’ '); @@ -4330,31 +3271,11 @@ class ClaudeOrchestrator { if (tierSessionId && !this.matchesWorkflowMode(tierSessionId)) continue; const tierBadge = tier ? `T${tier}` : ''; - const workspaceIdArg = this.escapeOnclickArg(String(this.currentWorkspace?.id || '')); - const worktreeIdArg = this.escapeOnclickArg(String(worktree.id || '')); - const displayNameArg = this.escapeOnclickArg(String(displayName || '')); - - const encode = (value) => { - try { return encodeURIComponent(String(value || '')); } catch { return ''; } - }; - const agentSessionEncoded = worktree.claude ? encode(worktree.claude.sessionId) : ''; - const serverSessionEncoded = worktree.server ? encode(worktree.server.sessionId) : ''; - const agentTitle = worktree.claude - ? (claudeVisible ? 'Hide agent terminal' : 'Show agent terminal') - : 'No agent terminal'; - const serverTitle = worktree.server - ? (serverVisible ? 'Hide server terminal' : 'Show server terminal') - : 'No server terminal'; - - const showRefresh = sidebarVisibility.refreshBranch !== false; - const showReadyForReview = sidebarVisibility.readyForReview !== false; - const showSessionToggles = sidebarVisibility.sessionVisibilityToggles !== false; - const showDelete = sidebarVisibility.deleteWorktree !== false; item.innerHTML = `
- +
${displayName}
@@ -4364,253 +3285,35 @@ class ClaudeOrchestrator {
- ${showRefresh ? ` - - ` : ''} - ${showReadyForReview ? ` - - ` : ''} - ${showSessionToggles ? ` - - - ` : ''} - ${showDelete ? ` - - ` : ''} -
-
- `; + + + +
+ + `; // Click handler is already attached via event delegation in setupEventListeners worktreeList.appendChild(item); } - const shortcuts = document.createElement('div'); - shortcuts.id = 'sidebar-project-shortcuts'; - shortcuts.className = 'sidebar-project-shortcuts'; - shortcuts.innerHTML = ``; - worktreeList.appendChild(shortcuts); - this.scheduleSidebarProjectShortcutsRender(); - worktreeList.scrollTop = previousScrollTop; } - getSidebarProjectShortcutsCollapsed() { - const fromServer = this.userSettings?.global?.ui?.sidebar?.projectShortcuts?.collapsed; - if (typeof fromServer === 'boolean') return fromServer; - try { - return localStorage.getItem('sidebar-project-shortcuts-collapsed') === 'true'; - } catch { - return false; - } - } - - setSidebarProjectShortcutsCollapsed(collapsed) { - const next = !!collapsed; - try { - localStorage.setItem('sidebar-project-shortcuts-collapsed', next ? 'true' : 'false'); - } catch { - // ignore - } - try { - this.updateGlobalUserSetting('ui.sidebar.projectShortcuts.collapsed', next); - } catch { - // ignore - } - this.scheduleSidebarProjectShortcutsRender(); - } - - scheduleSidebarProjectShortcutsRender({ force = false } = {}) { - if (this.sidebarProjectShortcutsRenderTimer) return; - this.sidebarProjectShortcutsRenderTimer = setTimeout(() => { - this.sidebarProjectShortcutsRenderTimer = null; - this.renderSidebarProjectShortcuts({ force }).catch(() => {}); - }, 160); - } - - async renderSidebarProjectShortcuts({ force = false } = {}) { - if (this.sidebarProjectShortcutsRenderInFlight) return; - this.sidebarProjectShortcutsRenderInFlight = true; - - try { - const container = document.getElementById('sidebar-project-shortcuts'); - if (!container) return; - - const [boardData, repos] = await Promise.all([ - this.getProjectsBoard({ force }).catch(() => null), - this.getScannedRepos({ force: false }).catch(() => []) - ]); - - const board = boardData?.board && typeof boardData.board === 'object' ? boardData.board : null; - if (!board || !Array.isArray(repos) || repos.length === 0) { - container.innerHTML = ''; - return; - } - - const repoByKey = new Map(); - for (const repo of repos) { - const key = this.normalizeProjectsBoardProjectKey(repo?.relativePath); - if (!key) continue; - if (!repoByKey.has(key)) repoByKey.set(key, repo); - } - - const getOrderIndex = (columnId) => { - const raw = board?.orderByColumn && typeof board.orderByColumn === 'object' ? board.orderByColumn[columnId] : null; - const order = Array.isArray(raw) ? raw : []; - const index = new Map(); - order.forEach((k, i) => { - const key = this.normalizeProjectsBoardProjectKey(k); - if (!key || index.has(key)) return; - index.set(key, i); - }); - return index; - }; - - const collect = (columnId) => { - const out = []; - for (const [key, repo] of repoByKey.entries()) { - const col = this.getProjectsBoardColumnForProjectKey(key, boardData); - if (col === columnId) out.push({ key, repo }); - } - const index = getOrderIndex(columnId); - out.sort((a, b) => { - const aRank = index.has(a.key) ? index.get(a.key) : Number.POSITIVE_INFINITY; - const bRank = index.has(b.key) ? index.get(b.key) : Number.POSITIVE_INFINITY; - if (aRank !== bRank) return aRank - bRank; - return String(a.repo?.name || '').localeCompare(String(b.repo?.name || '')); - }); - return out; - }; - - const shipNext = collect('next'); - const active = collect('active'); - const total = shipNext.length + active.length; - if (total === 0) { - container.innerHTML = ''; - return; - } - - const collapsed = this.getSidebarProjectShortcutsCollapsed(); - - const renderTile = (item) => { - const icon = this.getProjectIcon(item?.repo?.type); - const name = String(item?.repo?.name || item?.key || '').trim(); - const title = `${name} β€’ ${item?.key}`; - return ` - - `; - }; - - const renderGroup = (label, items) => { - if (!items.length) return ''; - return ` - - `; - }; - - container.innerHTML = ` - - `; - } finally { - this.sidebarProjectShortcutsRenderInFlight = false; - } - } - - async startProjectWorktreeFromBoardKey(projectKey) { - const key = this.normalizeProjectsBoardProjectKey(projectKey); - if (!key) return; - if (!this.socket) { - this.showToast?.('Socket not connected', 'error'); - return; - } - - let repos = []; - try { - repos = await this.getScannedRepos({ force: false }); - } catch { - repos = []; - } - - const repo = Array.isArray(repos) - ? repos.find((r) => this.normalizeProjectsBoardProjectKey(r?.relativePath) === key) - : null; - - if (!repo?.path) { - this.showToast?.(`Repo not found: ${key}`, 'error'); - return; - } - - const recommended = this.getRecommendedWorktree(repo); - if (!recommended?.id || !recommended?.path) { - this.showToast?.(`No free worktrees: ${repo.name || key}`, 'warning'); - return; - } - - await this.quickStartWorktree({ - repoPath: repo.path, - repoType: repo.type, - repoName: repo.name, - worktreeId: recommended.id, - worktreePath: recommended.path, - repositoryRoot: repo.path, - keepOpen: true, - explicitSelection: true - }); - } - getWorktreePathForSidebarEntry(worktree) { const workspace = this.currentWorkspace; if (!workspace || !worktree) return null; @@ -4792,8 +3495,8 @@ class ClaudeOrchestrator { return this.setWorktreeReadyForReview(worktreePath, !current); } - ensureFilterToggleExists() { - let filterToggle = document.getElementById('filter-toggle'); + ensureFilterToggleExists() { + let filterToggle = document.getElementById('filter-toggle'); if (!filterToggle) { // Create the filter toggle element @@ -4807,30 +3510,22 @@ class ClaudeOrchestrator { } // Always update the button content - const visibility = this.getUiVisibilityConfig().sidebar || {}; - const showActiveFilter = visibility.activeFilter !== false; - const showTierFilters = visibility.tierFilters !== false; - const activeBtn = showActiveFilter ? ` - - ` : ''; - const tierButtons = showTierFilters ? ` - - - - - - - ` : ''; - - filterToggle.innerHTML = ` -
- ${activeBtn} - ${tierButtons} -
- `; - } + filterToggle.innerHTML = ` +
+ +
+
+ + + + + + +
+ `; + } getServerStatusClass(sessionId) { const status = this.serverStatuses.get(sessionId); @@ -4959,10 +3654,6 @@ class ClaudeOrchestrator { } // Update the grid to show only these terminals - for (const sid of this.visibleTerminals) { - const session = this.sessions.get(sid); - this.setSessionVisibilityOverride(sid, null, { workspaceId: session?.workspace || null }); - } this.updateTerminalGrid(); this.buildSidebar(); } @@ -5009,7 +3700,7 @@ class ClaudeOrchestrator { console.log(`Found sessions for ${worktreeIdOrKey}:`, sessions); // Check if ANY session from this worktree is currently visible - const anyVisible = sessions.some((id) => this.isSessionVisibleByWorktreeSelection(id, this.sessions.get(id))); + const anyVisible = sessions.some(id => this.visibleTerminals.has(id)); // Log current state for debugging const claudeSessionId = sessions.find(id => id.includes('claude')); @@ -5026,8 +3717,6 @@ class ClaudeOrchestrator { // Show terminals - add back to visible set sessions.forEach(id => { this.visibleTerminals.add(id); - const session = this.sessions.get(id); - this.setSessionVisibilityOverride(id, null, { workspaceId: session?.workspace || null }); }); console.log(`Shown worktree ${worktreeIdOrKey}`); } @@ -5099,134 +3788,61 @@ class ClaudeOrchestrator { return; } - const visibleSet = new Set(this.activeView); - const groupMap = new Map(); - - sessionIds.forEach((sessionId, index) => { - const session = this.sessions.get(sessionId); - if (!session) return; - const key = this.getSessionWorktreeKey(sessionId, session) || sessionId; - let group = groupMap.get(key); - if (!group) { - group = { key, sessionIds: [], order: index }; - groupMap.set(key, group); - } - group.sessionIds.push(sessionId); - }); - - const groups = Array.from(groupMap.values()).sort((a, b) => a.order - b.order); - const rankSession = (sid, session) => { - const type = String(session?.type || '').trim().toLowerCase(); - if (type === 'server' || String(sid || '').endsWith('-server')) return 2; - if (type === 'codex' || String(sid || '').endsWith('-codex')) return 1; - return 0; - }; - const safePairId = (key) => { - if (typeof this.getDomSafeIdPart === 'function') { - return `terminal-pair-${this.getDomSafeIdPart(key)}`; - } - return `terminal-pair-${String(key || '').replace(/[^a-zA-Z0-9_-]/g, '_')}`; - }; - const ensurePairContainer = (key) => { - const id = safePairId(key); - let container = document.getElementById(id); - if (!container || !grid.contains(container)) { - container = document.createElement('div'); - container.id = id; - container.className = 'terminal-pair'; - container.dataset.worktreeKey = String(key || ''); - grid.appendChild(container); - } else { - grid.appendChild(container); - } - return container; - }; - - const activeGroupKeys = new Set(); - - groups.forEach((group) => { - const container = ensurePairContainer(group.key); - const ordered = group.sessionIds.slice().sort((a, b) => { - const ra = rankSession(a, this.sessions.get(a)); - const rb = rankSession(b, this.sessions.get(b)); - if (ra !== rb) return ra - rb; - return String(a).localeCompare(String(b)); - }); + // Set the data attribute for dynamic layout based on visible count + const visibleCount = this.activeView.length; + grid.setAttribute('data-visible-count', visibleCount); + // If the user has more than 16 visible terminals, fall back to a scrollable grid + // instead of clipping extra rows (which shows up as tiny β€œslivers” at the bottom). + grid.classList.toggle('terminal-grid-scrollable', visibleCount > 16); - let visibleCount = 0; + // CRITICAL: Don't destroy terminals with innerHTML = '' + // Instead, create missing terminals and hide/show existing ones - ordered.forEach((sessionId) => { - const session = this.sessions.get(sessionId); - let wrapper = this.getSessionWrapperElement(sessionId); - const docked = !!(wrapper && wrapper.classList.contains('review-console-terminal')); - const isVisible = visibleSet.has(sessionId) && !docked; - if (wrapper && !grid.contains(wrapper) && !docked) wrapper = null; - - console.log(`πŸ“ ${sessionId}: session=${!!session}, visible=${isVisible}, exists=${!!wrapper}`); - - if (session && isVisible) { - // Create wrapper if it doesn't exist - if (!wrapper) { - console.log(`βœ… Creating terminal element for: ${sessionId}`); - wrapper = this.createTerminalElement(sessionId, session); - if (wrapper) { - container.appendChild(wrapper); - console.log(`βœ… Appended terminal to grid: ${sessionId}`); - - // Initialize terminal for newly created element (scope query to wrapper/grid to avoid cross-tab collisions) - setTimeout(() => { - const terminalId = this.getSessionDomId('terminal', sessionId); - const terminalEl = document.getElementById(terminalId); - if (terminalEl && wrapper && wrapper.contains(terminalEl) && !this.terminalManager.terminals.has(sessionId)) { - this.terminalManager.createTerminal(sessionId, session); - } - }, 50); - } - } else { - // Show existing wrapper - wrapper.style.display = ''; - this.updateTerminalTicketLabel(sessionId); - - // Refit terminal if it exists - if (this.terminalManager.terminals.has(sessionId)) { - requestAnimationFrame(() => { - this.terminalManager.fitTerminal(sessionId); - }); - } - } - if (wrapper && wrapper.parentElement !== container) { - container.appendChild(wrapper); - } - visibleCount += 1; - } else if (wrapper && !docked) { - // Hide wrapper if not visible - wrapper.style.display = 'none'; - if (wrapper.parentElement !== container) { - container.appendChild(wrapper); + sessionIds.forEach((sessionId) => { + const session = this.sessions.get(sessionId); + const isVisible = this.isSessionVisibleInCurrentView(sessionId); + const wrapperId = this.getSessionDomId('wrapper', sessionId); + let wrapper = this.getSessionWrapperElement(sessionId); + if (wrapper && !grid.contains(wrapper)) wrapper = null; + + console.log(`πŸ“ ${sessionId}: session=${!!session}, visible=${isVisible}, exists=${!!wrapper}`); + + if (session && isVisible) { + // Create wrapper if it doesn't exist + if (!wrapper) { + console.log(`βœ… Creating terminal element for: ${sessionId}`); + wrapper = this.createTerminalElement(sessionId, session); + if (wrapper) { + grid.appendChild(wrapper); + console.log(`βœ… Appended terminal to grid: ${sessionId}`); + + // Initialize terminal for newly created element (scope query to wrapper/grid to avoid cross-tab collisions) + setTimeout(() => { + const terminalId = this.getSessionDomId('terminal', sessionId); + const terminalEl = document.getElementById(terminalId); + if (terminalEl && wrapper && wrapper.contains(terminalEl) && !this.terminalManager.terminals.has(sessionId)) { + this.terminalManager.createTerminal(sessionId, session); + } + }, 50); } - } else if (wrapper && docked && session) { + } else { + // Show existing wrapper + wrapper.style.display = ''; this.updateTerminalTicketLabel(sessionId); - } - }); - - container.classList.toggle('terminal-pair-single', visibleCount <= 1); - container.style.display = visibleCount ? '' : 'none'; - if (visibleCount) activeGroupKeys.add(group.key); - }); - // Remove stale pair containers (worktrees removed) - Array.from(grid.querySelectorAll('.terminal-pair')).forEach((container) => { - const key = String(container?.dataset?.worktreeKey || ''); - if (key && !groupMap.has(key)) container.remove(); + // Refit terminal if it exists + if (this.terminalManager.terminals.has(sessionId)) { + requestAnimationFrame(() => { + this.terminalManager.fitTerminal(sessionId); + }); + } + } + } else if (wrapper) { + // Hide wrapper if not visible + wrapper.style.display = 'none'; + } }); - // Set the data attribute for dynamic layout based on visible count - const visibleCount = activeGroupKeys.size; - grid.setAttribute('data-visible-count', visibleCount); - // If the user has more than 16 visible terminals, fall back to a scrollable grid - // instead of clipping extra rows (which shows up as tiny β€œslivers” at the bottom). - grid.classList.toggle('terminal-grid-scrollable', visibleCount > 16); - // Force a resize after everything is rendered to ensure terminals fit properly setTimeout(() => { this.resizeAllVisibleTerminals(); @@ -5710,38 +4326,32 @@ class ClaudeOrchestrator { this.lastInteractedSessionId = sessionId; }); - const sessionType = String(session?.type || '').trim().toLowerCase(); - const isAgentSession = sessionType === 'claude' || sessionType === 'codex'; - const isServerSession = sessionType === 'server'; + const isClaudeSession = session.type === 'claude'; + const isServerSession = session.type === 'server'; // Build display name with repository info for mixed-repo workspaces const repositoryName = this.extractRepositoryName(sessionId); const worktreeId = session.worktreeId; const displayName = repositoryName ? `${repositoryName}/${worktreeId}` : worktreeId.replace('work', ''); - const branchMeta = this.formatBranchLabel(session.branch || '', { context: 'terminal' }); - const branchRefreshId = (() => { try { return encodeURIComponent(String(sessionId || '')); } catch { return ''; } })(); - const terminalVisibility = this.getTerminalVisibilityConfig(); - const showBranchRefresh = terminalVisibility.branchRefresh !== false; - const showIntentHints = terminalVisibility.intentHints !== false; - const ticketMeta = this.getTicketMetaForSession(sessionId, session); - const ticketChip = ticketMeta?.label ? ( - ticketMeta.url - ? `🧾 ${this.escapeHtml(ticketMeta.label)}` - : `🧾 ${this.escapeHtml(ticketMeta.label)}` - ) : ''; - const intentHaikuId = showIntentHints ? this.getSessionDomId('intent-haiku', sessionId) : ''; - const intentHaikuText = showIntentHints ? this.escapeHtml(this.getSessionIntentHaikuText(sessionId)) : ''; + const branchMeta = this.formatBranchLabel(session.branch || '', { context: 'terminal' }); + const branchRefreshId = (() => { try { return encodeURIComponent(String(sessionId || '')); } catch { return ''; } })(); + const ticketMeta = this.getTicketMetaForSession(sessionId, session); + const ticketChip = ticketMeta?.label ? ( + ticketMeta.url + ? `🧾 ${this.escapeHtml(ticketMeta.label)}` + : `🧾 ${this.escapeHtml(ticketMeta.label)}` + ) : ''; wrapper.innerHTML = `
- ${isAgentSession ? 'πŸ€– Agent' : 'πŸ’» Server'} ${displayName} + ${isClaudeSession ? 'πŸ€– Agent' : 'πŸ’» Server'} ${displayName} ${this.escapeHtml(branchMeta.text || '')} - ${showBranchRefresh ? `` : ''} + ${ticketChip}
- ${isAgentSession ? ` + ${isClaudeSession ? ` ${this.getTierDropdownHTML(sessionId)} ${this.getServerQuickControlsHTMLForClaude(sessionId)} ${this.getWorktreeInspectorButtonHTML(sessionId)} @@ -5756,10 +4366,9 @@ class ClaudeOrchestrator { ` : ''}
- ${isAgentSession && showIntentHints ? `
${intentHaikuText}
` : ''}
- ${isAgentSession ? ` + ${isClaudeSession ? ` - `; - }).join('')} +
${title}
+
${due ? `${escapeHtml(due)} β€’ ` : ''}${last}
+
+ `; + }).join('')} `; @@ -21022,57 +18761,30 @@ class ClaudeOrchestrator { col.style.minWidth = ''; const baseWidth = col.getBoundingClientRect().width; - const cards = Array.from(cardsContainer.querySelectorAll('.task-card-board')); - const cardCount = cards.length; - if (cardCount === 0) { - col.style.setProperty('--tasks-card-columns', '1'); - col.style.setProperty('--tasks-card-rows', '1'); - return; - } - - const containerHeight = cardsContainer.clientHeight; - if (!containerHeight || containerHeight < 40) { - col.style.setProperty('--tasks-card-columns', '1'); - col.style.setProperty('--tasks-card-rows', '1'); - col.style.width = ''; - col.style.minWidth = ''; - - const tries = Number(col.dataset.tasksWrapExpandRetry || '0') || 0; - if (tries < 4) { - col.dataset.tasksWrapExpandRetry = String(tries + 1); - window.requestAnimationFrame(() => computeForColumn(col)); - } else { - delete col.dataset.tasksWrapExpandRetry; - } - return; - } - delete col.dataset.tasksWrapExpandRetry; - - const styles = window.getComputedStyle(cardsContainer); - const rowGap = Number.parseFloat(styles.rowGap || styles.gap || '0') || 0; - const columnGap = Number.parseFloat(styles.columnGap || styles.gap || '0') || 0; - const padLeft = Number.parseFloat(styles.paddingLeft || '0') || 0; - const padRight = Number.parseFloat(styles.paddingRight || '0') || 0; - - const measureSampleHeights = () => { - const maxSamples = Math.min(24, cardCount); - if (maxSamples <= 0) return []; - const step = Math.max(1, Math.floor(cardCount / maxSamples)); - const heights = []; - for (let i = 0; i < cardCount && heights.length < maxSamples; i += step) { - const h = cards[i]?.getBoundingClientRect?.().height; - if (h) heights.push(h); - } - return heights; - }; - - const heights = measureSampleHeights().sort((a, b) => a - b); - const median = heights.length ? heights[Math.floor(heights.length / 2)] : 80; - const denom = Math.max(1, median + rowGap); - let rowsFit = Math.max(1, Math.floor((containerHeight + rowGap) / denom)); - // In wrap+expand mode, prefer minimizing the number of columns by filling vertically first - // (as long as we still avoid vertical scrolling). - rowsFit = Math.min(rowsFit, cardCount); + const cards = Array.from(cardsContainer.querySelectorAll('.task-card-board')); + const cardCount = cards.length; + if (cardCount === 0) { + col.style.setProperty('--tasks-card-columns', '1'); + col.style.setProperty('--tasks-card-rows', '1'); + return; + } + + const containerHeight = cardsContainer.clientHeight; + if (!containerHeight || containerHeight < 40) return; + + const styles = window.getComputedStyle(cardsContainer); + const rowGap = Number.parseFloat(styles.rowGap || styles.gap || '0') || 0; + const columnGap = Number.parseFloat(styles.columnGap || styles.gap || '0') || 0; + const padLeft = Number.parseFloat(styles.paddingLeft || '0') || 0; + const padRight = Number.parseFloat(styles.paddingRight || '0') || 0; + const sample = cards.slice(0, Math.min(6, cardCount)); + const heights = sample.map(el => el.getBoundingClientRect().height).filter(Boolean); + const avg = heights.length ? (heights.reduce((a, b) => a + b, 0) / heights.length) : 80; + const denom = Math.max(1, avg + rowGap); + let rowsFit = Math.max(1, Math.floor((containerHeight + rowGap) / denom)); + // In wrap+expand mode, prefer minimizing the number of columns by filling vertically first + // (as long as we still avoid vertical scrolling). + rowsFit = Math.min(rowsFit, cardCount); const apply = (rows) => { const r = Math.max(1, Number(rows) || 1); @@ -21092,33 +18804,18 @@ class ClaudeOrchestrator { col.style.minWidth = `${Math.round(target)}px`; } }; - - apply(rowsFit); - const fits = () => (cardsContainer.scrollHeight <= cardsContainer.clientHeight + 1); - - // If we still overflow vertically, reduce rows (creating more columns) until we fit. - for (let attempt = 0; attempt < 24; attempt++) { - // Force reflow and then check overflow. - void cardsContainer.offsetHeight; - if (fits()) break; - rowsFit = Math.max(1, rowsFit - 1); - apply(rowsFit); - } - // If we fit, try to maximize rows (minimize columns) by filling vertically first. - for (let attempt = 0; attempt < 24; attempt++) { - if (rowsFit >= cardCount) break; - const next = rowsFit + 1; - apply(next); - void cardsContainer.offsetHeight; - if (fits()) { - rowsFit = next; - continue; - } - apply(rowsFit); - break; - } - }; + apply(rowsFit); + + // If we still overflow vertically, reduce rows (creating more columns) until we fit. + for (let attempt = 0; attempt < 24; attempt++) { + // Force reflow and then check overflow. + void cardsContainer.offsetHeight; + if (cardsContainer.scrollHeight <= cardsContainer.clientHeight + 1) break; + rowsFit = Math.max(1, rowsFit - 1); + apply(rowsFit); + } + }; // Clear variables for collapsed columns to avoid stale widths. for (const col of columns) { @@ -21487,7 +19184,6 @@ class ClaudeOrchestrator { const cards = await fetchCards({ refresh: force }); renderCards(cards); - syncBatchLaunchBtn(); } else { const isCombined = state.boardId === COMBINED_VIEW_ID; if (isCombined) { @@ -21820,7 +19516,6 @@ class ClaudeOrchestrator { state.listId = listEl.value || ''; localStorage.setItem('tasks-list', state.listId); await refreshAll({ force: true }); - syncBatchLaunchBtn(); }); if (searchEl) { @@ -21862,106 +19557,6 @@ class ClaudeOrchestrator { refreshBtn.addEventListener('click', () => refreshAll({ force: true })); } - const syncBatchLaunchBtn = () => { - if (!batchLaunchBtn) return; - const m = getBoardMapping(state.provider, state.boardId); - const hasList = state.listId && state.listId !== '__all__'; - const hasMapping = !!(m && m.localPath && isBoardEnabled(state.provider, state.boardId)); - batchLaunchBtn.style.display = (hasList && hasMapping) ? '' : 'none'; - }; - - if (batchLaunchBtn) { - batchLaunchBtn.addEventListener('click', async (e) => { - e.preventDefault(); - const m = getBoardMapping(state.provider, state.boardId); - if (!m || !m.localPath) return this.showToast('Board mapping not configured', 'warning'); - const listName = (state.lists || []).find(l => l.id === state.listId)?.name || state.listId; - const defaults = readLaunchDefaults({ mappingTier: m.defaultStartTier }); - const cardCount = (cardsEl.querySelectorAll('.task-card-row') || []).length; - - const dlg = document.createElement('div'); - dlg.className = 'modal-overlay'; - dlg.innerHTML = ` - `; - document.body.appendChild(dlg); - - dlg.querySelector('#bl-cancel').onclick = () => dlg.remove(); - dlg.querySelector('#bl-go').onclick = async () => { - const agent = dlg.querySelector('#bl-agent').value || null; - const tier = Number(dlg.querySelector('#bl-tier').value) || 3; - const move = dlg.querySelector('#bl-move').checked; - const limit = Number(dlg.querySelector('#bl-limit').value) || null; - const dryRun = dlg.querySelector('#bl-dry').checked; - dlg.querySelector('#bl-go').disabled = true; - dlg.querySelector('#bl-go').textContent = 'Launching...'; - - try { - const conventions = m.conventions || {}; - const doingListId = conventions.doingListId || null; - const res = await fetch('/api/tasks/batch-launch', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - provider: state.provider, - boardId: state.boardId, - listId: state.listId, - agentOverride: agent, - tierOverride: tier, - moveToListId: move ? doingListId : null, - limit, - dryRun - }) - }); - const data = await res.json(); - dlg.remove(); - if (data.dryRun) { - this.showToast(`Dry run: ${data.summary?.total || 0} cards would launch`, 'info'); - console.log('Batch launch dry run:', data); - } else if (data.success) { - this.showToast(`Launched ${data.summary?.launched || 0} of ${data.summary?.total || 0} cards`, 'success'); - if (data.failed?.length) this.showToast(`${data.failed.length} failed`, 'warning'); - refreshAll({ force: true }); - } else { - this.showToast(data.error || 'Batch launch failed', 'error'); - } - } catch (err) { - dlg.remove(); - this.showToast(String(err?.message || err), 'error'); - } - }; - }); - } - if (newCardBtn) { newCardBtn.addEventListener('click', (e) => { e.preventDefault(); @@ -23225,9 +20820,6 @@ class ClaudeOrchestrator { const state = { mode: 'mine', // mine | all - kindFilter: 'all', // all | pr | worktree | session - projectFilter: '', - prioritizeActive: localStorage.getItem('queue-prioritize-active') === 'true', query: '', tasks: [], selectedId: initialSelectedId, @@ -23236,7 +20828,6 @@ class ClaudeOrchestrator { unreviewedOnly: false, blockedOnly: localStorage.getItem('queue-blocked-only') === 'true', reviewRouteActive: false, - quickReview: false, autoOpenDiff: false, autoConsole: localStorage.getItem('queue-auto-console') === 'true', triageMode: localStorage.getItem('queue-triage') === 'true', @@ -23261,8 +20852,7 @@ class ClaudeOrchestrator { reviewTimer: { taskId: null, startedAtMs: null }, reviewerSpawning: new Set(), fixerSpawning: new Set(), - recheckSpawning: new Set(), - reprompted: [] + recheckSpawning: new Set() }; const loadSnoozes = () => { @@ -23317,21 +20907,6 @@ class ClaudeOrchestrator { if (this.queuePanelPreset && typeof this.queuePanelPreset === 'object') { const preset = this.queuePanelPreset; this.queuePanelPreset = null; - if (preset.mode !== undefined) { - state.mode = String(preset.mode || '').trim().toLowerCase() === 'all' ? 'all' : 'mine'; - } - if (preset.kindFilter !== undefined) { - const raw = String(preset.kindFilter || '').trim().toLowerCase(); - state.kindFilter = ['pr', 'worktree', 'session'].includes(raw) ? raw : 'all'; - } - if (preset.projectFilter !== undefined) { - state.projectFilter = String(preset.projectFilter || '').trim(); - } - if (preset.prioritizeActive !== undefined) { - state.prioritizeActive = !!preset.prioritizeActive; - try { localStorage.setItem('queue-prioritize-active', state.prioritizeActive ? 'true' : 'false'); } catch {} - } - if (preset.quickReview !== undefined) state.quickReview = !!preset.quickReview; if (preset.reviewTier !== undefined) { state.reviewTier = preset.reviewTier === 'all' || preset.reviewTier === 'none' ? preset.reviewTier @@ -23375,9 +20950,6 @@ class ClaudeOrchestrator {
-
@@ -23391,34 +20963,21 @@ class ClaudeOrchestrator {
-
- - - - - -
-
- Automation β–Ύ -
- - - - - - -
-
-
- Flows β–Ύ -
- - - - - -
-
+
+ + + + + + + + + + + + + +
@@ -23442,7 +21001,6 @@ class ClaudeOrchestrator { const listEl = modal.querySelector('#queue-list'); const detailEl = modal.querySelector('#queue-detail'); const searchEl = modal.querySelector('#queue-search'); - const projectFilterEl = modal.querySelector('#queue-project-filter'); const refreshBtn = modal.querySelector('#queue-refresh'); const pairingBtn = modal.querySelector('#queue-pairing'); const mineBtn = modal.querySelector('#queue-mode-mine'); @@ -23457,8 +21015,6 @@ class ClaudeOrchestrator { const unreviewedBtn = modal.querySelector('#queue-unreviewed'); const blockedBtn = modal.querySelector('#queue-blocked'); const triageBtn = modal.querySelector('#queue-triage'); - const kindPrBtn = modal.querySelector('#queue-kind-pr'); - const prioritizeActiveBtn = modal.querySelector('#queue-prioritize-active'); const autoDiffBtn = modal.querySelector('#queue-auto-diff'); const autoConsoleBtn = modal.querySelector('#queue-auto-console'); const autoNextBtn = modal.querySelector('#queue-auto-next'); @@ -23468,38 +21024,14 @@ class ClaudeOrchestrator { const conveyorT2Btn = modal.querySelector('#queue-conveyor-t2'); const conveyorT3Btn = modal.querySelector('#queue-conveyor-t3'); const reviewRouteBtn = modal.querySelector('#queue-review-route'); - const quickReviewBtn = modal.querySelector('#queue-quick-review'); const startReviewBtn = modal.querySelector('#queue-start-review'); const prevBtn = modal.querySelector('#queue-prev'); const nextBtn = modal.querySelector('#queue-next'); const closeBtn = modal.querySelector('#queue-close-btn'); - const automationMenuEl = modal.querySelector('#queue-automation-menu'); - const workflowsMenuEl = modal.querySelector('#queue-workflows-menu'); - const toolbarMenus = [automationMenuEl, workflowsMenuEl].filter(Boolean); - - if (toolbarMenus.length) { - toolbarMenus.forEach((menu) => { - menu.addEventListener('toggle', () => { - if (!menu.open) return; - toolbarMenus.forEach((other) => { - if (other === menu) return; - other.open = false; - }); - }); - }); - modal.addEventListener('click', (event) => { - const hit = event?.target?.closest?.('.tasks-toolbar-menu'); - if (hit) return; - toolbarMenus.forEach((menu) => { menu.open = false; }); - }); - } if (searchEl) { searchEl.value = String(state.query || ''); } - if (projectFilterEl) { - projectFilterEl.value = String(state.projectFilter || ''); - } const showPairingModal = async () => { const existing = document.getElementById('queue-pairing-modal'); @@ -23609,7 +21141,7 @@ class ClaudeOrchestrator { mineBtn.classList.toggle('active', state.mode === 'mine'); allBtn.classList.toggle('active', state.mode === 'all'); }; - setMode(state.mode); + setMode('mine'); const normalizeReviewTier = (tier) => { const raw = String(tier ?? '').trim().toLowerCase(); @@ -23634,8 +21166,6 @@ class ClaudeOrchestrator { triageBtn?.classList.toggle('active', !!state.triageMode); unreviewedBtn?.classList.toggle('active', !!state.unreviewedOnly); blockedBtn?.classList.toggle('active', !!state.blockedOnly); - kindPrBtn?.classList.toggle('active', state.kindFilter === 'pr'); - prioritizeActiveBtn?.classList.toggle('active', !!state.prioritizeActive); autoDiffBtn?.classList.toggle('active', !!state.autoOpenDiff); autoConsoleBtn?.classList.toggle('active', !!state.autoConsole); autoNextBtn?.classList.toggle('active', !!state.autoAdvance); @@ -23697,17 +21227,6 @@ class ClaudeOrchestrator { applyFiltersAndMaybeClampSelection(); }); - kindPrBtn?.addEventListener('click', () => { - state.kindFilter = state.kindFilter === 'pr' ? 'all' : 'pr'; - applyFiltersAndMaybeClampSelection(); - }); - - prioritizeActiveBtn?.addEventListener('click', () => { - state.prioritizeActive = !state.prioritizeActive; - try { localStorage.setItem('queue-prioritize-active', state.prioritizeActive ? 'true' : 'false'); } catch {} - applyFiltersAndMaybeClampSelection(); - }); - autoDiffBtn?.addEventListener('click', () => { state.autoOpenDiff = !state.autoOpenDiff; syncReviewControlsUI(); @@ -23752,7 +21271,7 @@ class ClaudeOrchestrator { if (state.selectedId) renderDetail(getTaskById(state.selectedId)); }); - const startReviewRoute = async () => { + const startReviewRoute = () => { state.reviewRouteActive = true; state.reviewActive = true; state.reviewTier = 'all'; @@ -23767,7 +21286,6 @@ class ClaudeOrchestrator { try { localStorage.setItem('queue-auto-advance', 'true'); } catch {} try { localStorage.setItem('queue-triage', 'false'); } catch {} try { localStorage.setItem('queue-blocked-only', 'false'); } catch {} - try { await this.applyReviewRouteUiDefaults(); } catch {} syncReviewControlsUI(); renderList(); @@ -23777,55 +21295,7 @@ class ClaudeOrchestrator { this.showToast('No Tier 3/4 unreviewed items in review route', 'info'); return; } - const first = ordered[0]; - selectById(first.id, { allowAutoOpenDiff: false }); - const wantsConsole = !!(first?.sessionId || first?.worktreePath || (first?.kind === 'pr' && String(first?.url || '').trim())); - if (state.autoConsole && wantsConsole) { - this.openReviewConsoleForTask(first).catch((error) => { - console.error('Failed to auto-open review console for review route:', error); - }); - } - }; - - const startQuickReview = async () => { - const defaults = this.getReviewInboxDefaults('quickReview'); - state.reviewRouteActive = false; - state.reviewActive = true; - state.reviewTier = defaults.reviewTier; - state.tierSet = defaults.tierSet; - state.triageMode = false; - state.unreviewedOnly = defaults.unreviewedOnly; - state.blockedOnly = false; - state.kindFilter = defaults.kind; - state.prioritizeActive = defaults.prioritizeActive; - state.autoConsole = defaults.autoConsole; - state.autoOpenDiff = false; - state.autoAdvance = defaults.autoAdvance; - state.mode = defaults.mode; - state.projectFilter = defaults.project || ''; - try { localStorage.setItem('queue-auto-console', defaults.autoConsole ? 'true' : 'false'); } catch {} - try { localStorage.setItem('queue-auto-advance', defaults.autoAdvance ? 'true' : 'false'); } catch {} - try { localStorage.setItem('queue-triage', 'false'); } catch {} - try { localStorage.setItem('queue-blocked-only', 'false'); } catch {} - try { localStorage.setItem('queue-prioritize-active', defaults.prioritizeActive ? 'true' : 'false'); } catch {} - setMode(defaults.mode); - - syncReviewControlsUI(); - renderList(); - - const ordered = getOrderedTasks(getFilteredTasks()); - if (!ordered.length) { - this.showToast('No Tier 3/4 unreviewed PRs in Quick Review', 'info'); - return; - } - const first = ordered[0]; - selectById(first.id, { allowAutoOpenDiff: false }); - const wantsConsole = !!(first?.sessionId || first?.worktreePath || (first?.kind === 'pr' && String(first?.url || '').trim())); - if (state.autoConsole && wantsConsole) { - this.openReviewConsoleForTask(first).catch((error) => { - console.error('Failed to auto-open review console for quick review:', error); - }); - } + selectById(ordered[0].id, { allowAutoOpenDiff: false }); }; startReviewBtn?.addEventListener('click', async () => { @@ -23885,11 +21355,7 @@ class ClaudeOrchestrator { }); reviewRouteBtn?.addEventListener('click', () => { - startReviewRoute().catch((e) => this.showToast(String(e?.message || e), 'error')); - }); - - quickReviewBtn?.addEventListener('click', () => { - startQuickReview().catch((e) => this.showToast(String(e?.message || e), 'error')); + startReviewRoute(); }); const maybeAutoAdvanceAfterReview = (currentTaskId) => { @@ -23907,79 +21373,6 @@ class ClaudeOrchestrator { selectById(ordered[nextIndex].id, { allowAutoOpenDiff: true }); }; - const normalizeProjectKey = (value) => String(value || '').trim().toLowerCase(); - const extractRepoName = (value) => { - const raw = String(value || '').trim(); - if (!raw) return ''; - const parts = raw.replace(/\\/g, '/').split('/').filter(Boolean); - return parts[parts.length - 1] || raw; - }; - - const updateProjectFilterOptions = () => { - if (!projectFilterEl) return; - const options = new Map(); - for (const t of (Array.isArray(state.tasks) ? state.tasks : [])) { - if (t?.kind !== 'pr') continue; - const repo = String(t?.repository || '').trim(); - const project = String(t?.project || '').trim(); - const label = repo || project; - if (!label) continue; - options.set(normalizeProjectKey(label), label); - } - const sorted = Array.from(options.values()).sort((a, b) => String(a).localeCompare(String(b))); - const current = normalizeProjectKey(state.projectFilter); - projectFilterEl.innerHTML = `` + sorted - .map((label) => ``) - .join(''); - if (current && options.has(current)) { - projectFilterEl.value = options.get(current); - state.projectFilter = options.get(current); - } else if (!state.projectFilter) { - projectFilterEl.value = ''; - } else if (!options.has(current)) { - projectFilterEl.value = ''; - state.projectFilter = ''; - } - }; - - const pruneReprompted = () => { - if (!Array.isArray(state.reprompted) || state.reprompted.length === 0) return; - const liveIds = new Set((Array.isArray(state.tasks) ? state.tasks : []).map((t) => String(t?.id || '').trim()).filter(Boolean)); - state.reprompted = state.reprompted.filter((entry) => liveIds.has(String(entry?.taskId || '').trim())); - }; - - const registerRepromptedTask = ({ taskId, worktreeId } = {}) => { - const id = String(taskId || '').trim(); - if (!id) return; - const wt = String(worktreeId || '').trim() || null; - const next = (Array.isArray(state.reprompted) ? state.reprompted : []).filter((entry) => String(entry?.taskId || '').trim() !== id); - next.unshift({ taskId: id, worktreeId: wt, createdAtMs: Date.now() }); - state.reprompted = next.slice(0, 50); - }; - - const isReadyStatus = (status) => { - const s = String(status || '').trim().toLowerCase(); - return s === 'waiting' || s === 'ready' || s === 'ready-new'; - }; - - const findReadyRepromptedTaskId = () => { - if (!Array.isArray(state.reprompted) || state.reprompted.length === 0) return ''; - const readyWorktreeIds = new Set(); - for (const [sid, session] of this.sessions) { - if (!this.isAgentSession(sid)) continue; - if (!isReadyStatus(session?.status)) continue; - const wt = String(session?.worktreeId || '').trim(); - if (wt) readyWorktreeIds.add(wt); - } - for (const entry of state.reprompted) { - const id = String(entry?.taskId || '').trim(); - const wt = String(entry?.worktreeId || '').trim(); - if (!id || !wt) continue; - if (readyWorktreeIds.has(wt)) return id; - } - return ''; - }; - const calcTierCounts = (tasks) => { const counts = { 1: 0, 2: 0, 3: 0, 4: 0, none: 0 }; for (const t of tasks) { @@ -23993,20 +21386,6 @@ class ClaudeOrchestrator { const getFilteredTasks = () => { const q = String(state.query || '').trim().toLowerCase(); return (Array.isArray(state.tasks) ? state.tasks : []).filter((t) => { - if (state.kindFilter !== 'all') { - if (String(t?.kind || '').trim() !== state.kindFilter) return false; - } - const projectFilter = normalizeProjectKey(state.projectFilter); - if (projectFilter) { - const repo = normalizeProjectKey(t?.repository || ''); - const project = normalizeProjectKey(t?.project || ''); - const repoName = normalizeProjectKey(extractRepoName(t?.repository || '')); - const matches = repo === projectFilter - || project === projectFilter - || repoName === projectFilter - || (repo && repo.endsWith(`/${projectFilter}`)); - if (!matches) return false; - } const tier = Number(t?.record?.tier); if (state.tierSet && Array.isArray(state.tierSet) && state.tierSet.length) { if (!state.tierSet.includes(tier)) return false; @@ -24051,78 +21430,7 @@ class ClaudeOrchestrator { }); }; - const buildActiveIndex = () => { - const activeSessionIds = new Set(); - const activeWorktreeIds = new Set(); - const activeWorktreePaths = new Set(); - const activeRepoNames = new Set(); - const activeRepoSlugs = new Set(); - - for (const [sid, session] of this.sessions) { - if (!this.isAgentSession(sid)) continue; - const status = String(session?.status || '').trim().toLowerCase(); - if (status === 'exited') continue; - const hasActivity = this.sessionActivity.get(sid) === 'active' || status === 'busy' || status === 'waiting'; - if (!hasActivity) continue; - - activeSessionIds.add(String(sid)); - - const worktreeId = String(session?.worktreeId || '').trim(); - if (worktreeId) activeWorktreeIds.add(worktreeId); - - const worktreePath = String(session?.config?.cwd || '').trim(); - if (worktreePath) activeWorktreePaths.add(worktreePath); - - const repoName = String(session?.repositoryName || '').trim(); - if (repoName) activeRepoNames.add(normalizeProjectKey(repoName)); - - const repoRoot = String(session?.repositoryRoot || '').trim(); - const repoRootName = extractRepoName(repoRoot); - if (repoRootName) activeRepoNames.add(normalizeProjectKey(repoRootName)); - - const repoSlug = String(session?.repositorySlug || '').trim(); - if (repoSlug) activeRepoSlugs.add(normalizeProjectKey(repoSlug)); - } - - return { activeSessionIds, activeWorktreeIds, activeWorktreePaths, activeRepoNames, activeRepoSlugs }; - }; - - const getActiveScoreForTask = (t, index) => { - if (!index) return 0; - if (t?.kind === 'session') { - const sid = String(t?.sessionId || '').trim(); - return (sid && index.activeSessionIds.has(sid)) ? 3 : 0; - } - if (t?.kind === 'worktree') { - const path = String(t?.worktreePath || '').trim(); - if (path && index.activeWorktreePaths.has(path)) return 2; - const worktreeId = String(t?.worktreeId || '').trim(); - if (worktreeId && index.activeWorktreeIds.has(worktreeId)) return 2; - return 0; - } - if (t?.kind === 'pr') { - const rec = (t?.record && typeof t.record === 'object') ? t.record : {}; - const worktreeIds = [ - rec.reviewerWorktreeId, - rec.fixerWorktreeId, - rec.recheckWorktreeId, - rec.overnightWorktreeId - ].map((v) => String(v || '').trim()).filter(Boolean); - if (worktreeIds.some((id) => index.activeWorktreeIds.has(id))) return 3; - - const repoSlug = normalizeProjectKey(t?.repository || ''); - if (repoSlug && index.activeRepoSlugs.has(repoSlug)) return 2; - - const repoName = normalizeProjectKey(extractRepoName(t?.repository || '')); - const projectName = normalizeProjectKey(t?.project || ''); - if ((repoName && index.activeRepoNames.has(repoName)) || (projectName && index.activeRepoNames.has(projectName))) return 2; - } - return 0; - }; - const getOrderedTasks = (tasks) => { - const activeIndex = state.prioritizeActive ? buildActiveIndex() : null; - const activeScore = (t) => getActiveScoreForTask(t, activeIndex); const unblocked = []; const blocked = []; for (const t of (Array.isArray(tasks) ? tasks : [])) { @@ -24148,9 +21456,6 @@ class ClaudeOrchestrator { const triageSort = (arr) => { return [...arr].sort((a, b) => { - const sa = activeScore(a); - const sb = activeScore(b); - if (sb !== sa) return sb - sa; const pa = triagePriority(a); const pb = triagePriority(b); if (pa !== pb) return pa - pb; @@ -24163,19 +21468,7 @@ class ClaudeOrchestrator { if (!state.reviewActive) { if (state.triageMode) return triageSort(unblocked).concat(triageSort(blocked)); - const sortByActive = (arr) => { - if (!state.prioritizeActive) return arr; - return [...arr].sort((a, b) => { - const sa = activeScore(a); - const sb = activeScore(b); - if (sb !== sa) return sb - sa; - const at = parseUpdatedMs(a); - const bt = parseUpdatedMs(b); - if (bt !== at) return bt - at; - return String(a?.title || a?.id || '').localeCompare(String(b?.title || b?.id || '')); - }); - }; - return sortByActive(unblocked).concat(sortByActive(blocked)); + return unblocked.concat(blocked); } const riskToScore = (raw) => { @@ -24203,9 +21496,6 @@ class ClaudeOrchestrator { const reviewSort = (arr) => { return [...arr].sort((a, b) => { - const sa = activeScore(a); - const sb = activeScore(b); - if (sb !== sa) return sb - sa; const ra = overallRiskScore(a); const rb = overallRiskScore(b); if (rb !== ra) return rb - ra; @@ -24488,8 +21778,6 @@ class ClaudeOrchestrator { const data = await res.json().catch(() => ({})); if (!res.ok) throw new Error(data?.error || 'Failed to load queue'); state.tasks = data.tasks || []; - updateProjectFilterOptions(); - pruneReprompted(); const selectedStillExists = !!(state.selectedId && getTaskById(state.selectedId)); if (!selectedStillExists) { const ordered = getOrderedTasks(getFilteredTasks()); @@ -25563,7 +22851,6 @@ class ClaudeOrchestrator { ${hasPR ? `` : ''} ${hasPR ? `` : ''} ${hasPR ? `` : ''} - ${hasPR ? `` : ''} ${hasPR ? `` : ''}
@@ -25852,7 +23139,6 @@ class ClaudeOrchestrator { const spawnOvernightBtn = detailEl.querySelector('#queue-spawn-overnight'); const spawnReviewerBtn = detailEl.querySelector('#queue-spawn-reviewer'); const spawnFixerBtn = detailEl.querySelector('#queue-spawn-fixer'); - const repromptBtn = detailEl.querySelector('#queue-reprompt'); const spawnRecheckBtn = detailEl.querySelector('#queue-spawn-recheck'); const timerStartBtn = detailEl.querySelector('#queue-review-timer-start'); const timerStopBtn = detailEl.querySelector('#queue-review-timer-stop'); @@ -26995,40 +24281,6 @@ class ClaudeOrchestrator { } }); - repromptBtn?.addEventListener('click', async () => { - try { - repromptBtn.disabled = true; - const notes = String(notesEl?.value || '').trim(); - if (!notes && !window.confirm('Reprompt without Notes?')) return; - await saveNotes(); - const existingFixerId = String(t?.record?.fixerWorktreeId || '').trim(); - const info = await this.spawnFixAgentForPRTask(t, { - tier: 2, - agentId: 'claude', - mode: 'fresh', - yolo: true, - notes, - worktreeId: existingFixerId || null - }); - if (info) { - const worktreeId = info.worktreeId || existingFixerId || null; - const rec = await upsertRecord(t.id, { - fixerSpawnedAt: new Date().toISOString(), - fixerWorktreeId: worktreeId - }); - updateTaskRecordInState(t.id, rec); - registerRepromptedTask({ taskId: t.id, worktreeId }); - renderList(); - renderDetail(getTaskById(t.id)); - this.showToast('Reprompted fixer started', 'success'); - } - } catch (e) { - this.showToast(String(e?.message || e), 'error'); - } finally { - if (repromptBtn) repromptBtn.disabled = false; - } - }); - spawnRecheckBtn?.addEventListener('click', async () => { try { spawnRecheckBtn.disabled = true; @@ -27312,11 +24564,6 @@ class ClaudeOrchestrator { if (state.selectedId) renderDetail(getTaskById(state.selectedId)); }); - projectFilterEl?.addEventListener('change', () => { - state.projectFilter = String(projectFilterEl.value || ''); - applyFiltersAndMaybeClampSelection(); - }); - refreshBtn.addEventListener('click', async () => { refreshBtn.disabled = true; try { @@ -27346,17 +24593,6 @@ class ClaudeOrchestrator { }); const navigate = (dir) => { - if (dir > 0) { - const readyId = findReadyRepromptedTaskId(); - if (readyId && readyId !== state.selectedId) { - state.reprompted = state.reprompted.filter((entry) => String(entry?.taskId || '').trim() !== readyId); - selectById(readyId, { allowAutoOpenDiff: true }); - return; - } - if (readyId && readyId === state.selectedId) { - state.reprompted = state.reprompted.filter((entry) => String(entry?.taskId || '').trim() !== readyId); - } - } const ordered = getOrderedTasks(getFilteredTasks()); if (!ordered.length) return; const currentIndex = state.selectedId ? ordered.findIndex(t => t.id === state.selectedId) : -1; @@ -27389,15 +24625,11 @@ class ClaudeOrchestrator { worktreePath: pick(t?.worktreePath) || null, tier: rec?.tier ?? null, changeRisk: pick(rec?.changeRisk) || null, - pFailFirstPass: Number.isFinite(Number(rec?.pFailFirstPass)) ? Number(rec.pFailFirstPass) : null, - verifyMinutes: Number.isFinite(Number(rec?.verifyMinutes)) ? Number(rec.verifyMinutes) : null, - updatedAt: pick(rec?.updatedAt || t?.updatedAt || '') || null, claimedBy: pick(rec?.claimedBy) || null, assignedTo: pick(rec?.assignedTo) || null, reviewed: !!rec?.reviewedAt, done: !!rec?.doneAt, - outcome: pick(rec?.reviewOutcome) || null, - blockedCount: Number(t?.dependencySummary?.blocked || 0) || 0 + outcome: pick(rec?.reviewOutcome) || null }; }); }, @@ -27434,47 +24666,6 @@ class ClaudeOrchestrator { return false; } }, - selectByPrRef: async ({ number, repo } = {}) => { - try { - const rawNumber = String(number || '').trim(); - if (!/^[0-9]+$/.test(rawNumber)) { - this.showToast?.('Missing or invalid PR number', 'warning'); - return false; - } - const rawRepo = String(repo || '').trim().toLowerCase(); - const repoNeedle = rawRepo.includes('/') ? rawRepo.split('/').filter(Boolean).join('/') : rawRepo; - if (!Array.isArray(state.tasks) || state.tasks.length === 0) { - await fetchTasks(); - } - - const prNumberNeedle = `/pull/${rawNumber}`; - const hits = (state.tasks || []).filter((task) => String(task?.url || '').includes(prNumberNeedle)); - if (hits.length === 0) { - this.showToast?.(`PR #${rawNumber} not found in Queue`, 'warning'); - return false; - } - - let hit = hits[0]; - if (repoNeedle) { - hit = hits.find((task) => { - const url = String(task?.url || '').toLowerCase(); - if (!url) return false; - if (repoNeedle.includes('/')) return url.includes(`/${repoNeedle}/pull/`); - return url.includes(`/${repoNeedle}/pull/`); - }) || null; - if (!hit?.id) { - this.showToast?.(`PR #${rawNumber} found, but not in repo "${repoNeedle}"`, 'warning'); - return false; - } - } - - selectById(String(hit.id), { allowAutoOpenDiff: true }); - return true; - } catch (e) { - this.showToast?.(String(e?.message || e), 'error'); - return false; - } - }, selectByTicket: async ({ ticket } = {}) => { try { const raw = String(ticket || '').trim(); @@ -28495,11 +25686,8 @@ class ClaudeOrchestrator { await fetchTasks(); // Initial render respects triage mode + tierSet presets. applyFiltersAndMaybeClampSelection({ renderSelectedDetail: false }); - if (state.quickReview) { - state.quickReview = false; - startQuickReview().catch((e) => this.showToast(String(e?.message || e), 'error')); - } else if (state.reviewRouteActive) { - startReviewRoute().catch((e) => this.showToast(String(e?.message || e), 'error')); + if (state.reviewRouteActive) { + startReviewRoute(); } else if (state.selectedId) { selectById(state.selectedId, { allowAutoOpenDiff: state.reviewActive }); } @@ -28719,10 +25907,7 @@ class ClaudeOrchestrator { if (existing) existing.remove(); const categories = {}; - this.addWorktreeModalReposRaw = Array.isArray(allRepos) ? allRepos : []; - const filteredRepos = this.filterReposForWorktreeMenus(this.addWorktreeModalReposRaw); - - filteredRepos.forEach(repo => { + allRepos.forEach(repo => { if (!categories[repo.category]) categories[repo.category] = []; categories[repo.category].push(repo); }); @@ -28744,14 +25929,6 @@ class ClaudeOrchestrator { - - -
- - -
Create @@ -28987,16 +26136,6 @@ class ClaudeOrchestrator { favoritesOnlyCheckbox.checked = !!this.quickWorktreeFavoritesOnly; } - const menuVisibilityPrefs = this.getProjectsBoardMenuVisibilityPrefs(); - const showArchivedEl = modal.querySelector('#quick-show-archived-projects'); - if (showArchivedEl) { - showArchivedEl.checked = !!menuVisibilityPrefs.showArchived; - } - const showDoneEl = modal.querySelector('#quick-show-done-projects'); - if (showDoneEl) { - showDoneEl.checked = !!menuVisibilityPrefs.showDone; - } - const keepOpenCheckbox = modal.querySelector('#worktree-modal-keep-open'); if (keepOpenCheckbox) { keepOpenCheckbox.checked = this.getWorktreeModalKeepOpen(); @@ -29048,16 +26187,6 @@ class ClaudeOrchestrator { this.renderQuickWorktreeRepoList(); return; } - - if (e.target && (e.target.id === 'quick-show-archived-projects' || e.target.id === 'quick-show-done-projects')) { - const next = { - showArchived: !!modal.querySelector('#quick-show-archived-projects')?.checked, - showDone: !!modal.querySelector('#quick-show-done-projects')?.checked - }; - this.setProjectsBoardMenuVisibilityPrefs(next); - this.renderQuickWorktreeRepoList(); - return; - } }); if (advancedBtn) { @@ -29149,7 +26278,6 @@ class ClaudeOrchestrator { const serverUrl = window.location.port === '2080' ? 'http://localhost:3000' : window.location.origin; const response = await fetch(`${serverUrl}/api/workspaces/scan-repos`); const allRepos = await response.json(); - await this.getProjectsBoard({ force: false }).catch(() => {}); this.showAdvancedAddWorktreeModal(allRepos); } catch (error) { console.error('Failed to fetch repositories:', error); @@ -29162,7 +26290,6 @@ class ClaudeOrchestrator { if (!listEl) return; try { - await this.getProjectsBoard({ force: false }).catch(() => {}); const serverUrl = window.location.port === '2080' ? 'http://localhost:3000' : window.location.origin; const response = await fetch(`${serverUrl}/api/workspaces/scan-repos`); const repos = await response.json(); @@ -29225,9 +26352,7 @@ class ClaudeOrchestrator { ? filtered.filter(r => favoritesSet.has(r.path)) : filtered; - const filteredByBoard = this.filterReposForWorktreeMenus(filteredFavorites); - - const sorted = filteredByBoard.sort((a, b) => { + const sorted = filteredFavorites.sort((a, b) => { if (this.quickWorktreeSortMode === 'created') { const aCreated = a.createdMs || 0; const bCreated = b.createdMs || 0; @@ -30374,6 +27499,21 @@ class ClaudeOrchestrator { } } + // Also check mixed-repo workspace config for explicitly assigned worktrees + if (this.currentWorkspace.terminals && Array.isArray(this.currentWorkspace.terminals)) { + const repoNameMatch = repoNameOverride ? repoNameOverride.toLowerCase() : ''; + return this.currentWorkspace.terminals.some(terminal => { + if (terminal.worktree !== worktreeId) return false; + if (terminal.repository?.path === repoPath) { + return true; // This worktree is assigned in workspace config + } + if (repoNameMatch && (terminal.repository?.name || '').toLowerCase() === repoNameMatch) { + return true; // Match by repo name when paths differ + } + return false; + }); + } + // No active sessions for this worktree - it's available return false; } @@ -30387,14 +27527,7 @@ class ClaudeOrchestrator { : []; if (!ids.length) return; - const agentSessionIds = ids.filter((id) => { - const sid = String(id || '').trim(); - if (!sid) return false; - if (/-claude$|-codex$/.test(sid)) return true; - const sess = this.sessions.get(sid); - const type = String(sess?.type || '').trim().toLowerCase(); - return type === 'claude' || type === 'codex'; - }); + const agentSessionIds = ids.filter(id => id.includes('-claude')); if (!agentSessionIds.length) return; // Update local state immediately so gating/UI reads the tier before async persistence completes. @@ -30761,55 +27894,23 @@ class ClaudeOrchestrator { } } - scheduleAutoPromptFallback(sessionId, agentId) { - const sid = String(sessionId || '').trim(); - if (!sid) return; - - const pending = this.pendingAutoPrompts.get(sid); - if (!pending || pending.sentAt) return; - - const agent = String(agentId || pending.agentId || '').trim().toLowerCase(); - const delayMs = agent === 'codex' ? 15_000 : 8_000; - - const existing = this.pendingAutoPromptFallbackTimers.get(sid); - if (existing) clearTimeout(existing); - - const timer = setTimeout(() => { - this.pendingAutoPromptFallbackTimers.delete(sid); - this.maybeAutoSendPrompt(sid, 'waiting', { force: true }); - }, delayMs); - - this.pendingAutoPromptFallbackTimers.set(sid, timer); - } - - maybeAutoSendPrompt(sessionId, status, { force = false } = {}) { + maybeAutoSendPrompt(sessionId, status) { const pending = this.pendingAutoPrompts.get(sessionId); if (!pending || pending.sentAt) return; const normalized = String(status || '').toLowerCase(); - if (!force && normalized !== 'waiting') return; + if (normalized !== 'waiting') return; if (!this.socket || !this.socket.connected) return; const text = String(pending.text || '').trim(); if (!text) { - const timer = this.pendingAutoPromptFallbackTimers.get(sessionId); - if (timer) { - clearTimeout(timer); - this.pendingAutoPromptFallbackTimers.delete(sessionId); - } this.pendingAutoPrompts.delete(sessionId); return; } const payload = text.endsWith('\n') ? text : `${text}\n`; this.socket.emit('terminal-input', { sessionId, data: payload }); - this.socket.emit('command-executed', { sessionId, command: text }); pending.sentAt = Date.now(); this.pendingAutoPrompts.set(sessionId, pending); - const timer = this.pendingAutoPromptFallbackTimers.get(sessionId); - if (timer) { - clearTimeout(timer); - this.pendingAutoPromptFallbackTimers.delete(sessionId); - } // Best-effort telemetry: record when we auto-sent a prompt (session task record). this.upsertTaskRecord(`session:${sessionId}`, { @@ -30849,108 +27950,6 @@ class ClaudeOrchestrator { return arr; } - invalidateProjectsBoardCache() { - this.projectsBoardCache = { value: null, fetchedAt: 0 }; - } - - getProjectsBoardMenuVisibilityPrefs() { - const fromServer = this.userSettings?.global?.ui?.projects?.board?.menus; - const serverShowArchived = typeof fromServer?.showArchived === 'boolean' ? fromServer.showArchived : null; - const serverShowDone = typeof fromServer?.showDone === 'boolean' ? fromServer.showDone : null; - - const fromLocalStorage = (key) => { - try { return localStorage.getItem(key) === 'true'; } catch { return false; } - }; - - return { - showArchived: serverShowArchived === null ? fromLocalStorage('projects-board-menus-show-archived') : serverShowArchived, - showDone: serverShowDone === null ? fromLocalStorage('projects-board-menus-show-done') : serverShowDone - }; - } - - async setProjectsBoardMenuVisibilityPrefs(next) { - const desired = next && typeof next === 'object' ? next : {}; - const prefs = { - showArchived: !!desired.showArchived, - showDone: !!desired.showDone - }; - - try { localStorage.setItem('projects-board-menus-show-archived', prefs.showArchived ? 'true' : 'false'); } catch {} - try { localStorage.setItem('projects-board-menus-show-done', prefs.showDone ? 'true' : 'false'); } catch {} - - try { - await this.updateGlobalUserSetting('ui.projects.board.menus', prefs); - } catch { - // ignore - } - - // Re-render any open worktree add modals to reflect updated filtering. - try { this.refreshWorktreeAddModals(); } catch {} - } - - normalizeProjectsBoardProjectKey(value) { - return String(value || '').trim().replace(/\\/g, '/'); - } - - normalizeProjectsBoardColumnId(value) { - const raw = String(value || '').trim().toLowerCase(); - if (!raw) return ''; - if (raw === 'archive') return 'archived'; - return raw; - } - - async getProjectsBoard({ force = false } = {}) { - const now = Date.now(); - const ttlMs = 15_000; - if (!force && this.projectsBoardCache?.value && (now - (this.projectsBoardCache.fetchedAt || 0) < ttlMs)) { - return this.projectsBoardCache.value; - } - - const url = new URL('/api/projects/board', window.location.origin); - if (force) url.searchParams.set('refresh', 'true'); - const res = await fetch(url.toString()); - const data = await res.json().catch(() => ({})); - if (!res.ok || !data?.ok) throw new Error(String(data?.error || 'Failed to load projects board')); - - this.projectsBoardCache = { value: data, fetchedAt: now }; - return data; - } - - getProjectsBoardColumnForProjectKey(projectKey, boardData = null) { - const key = this.normalizeProjectsBoardProjectKey(projectKey); - if (!key) return 'backlog'; - const board = (boardData?.board && typeof boardData.board === 'object') - ? boardData.board - : (this.projectsBoardCache?.value?.board && typeof this.projectsBoardCache.value.board === 'object' ? this.projectsBoardCache.value.board : null); - const mapping = board?.projectToColumn && typeof board.projectToColumn === 'object' ? board.projectToColumn : {}; - const mapped = this.normalizeProjectsBoardColumnId(mapping[key]); - return mapped || 'backlog'; - } - - getProjectsBoardColumnForRepo(repo, boardData = null) { - const key = this.normalizeProjectsBoardProjectKey(repo?.relativePath || repo?.key); - if (!key) return 'backlog'; - return this.getProjectsBoardColumnForProjectKey(key, boardData); - } - - filterReposForWorktreeMenus(repos, boardData = null) { - const prefs = this.getProjectsBoardMenuVisibilityPrefs(); - const rows = Array.isArray(repos) ? repos : []; - if (!rows.length) return rows; - - const board = (boardData?.board && typeof boardData.board === 'object') - ? boardData - : (this.projectsBoardCache?.value?.board ? this.projectsBoardCache.value : null); - if (!board?.board) return rows; - - return rows.filter((repo) => { - const col = this.getProjectsBoardColumnForRepo(repo, board); - if (!prefs.showArchived && col === 'archived') return false; - if (!prefs.showDone && col === 'done') return false; - return true; - }); - } - async getGitHubRepos({ force = false, limit = 500, owner = null } = {}) { const now = Date.now(); const ttlMs = 60_000; @@ -31375,6 +28374,7 @@ class ClaudeOrchestrator { const agentConfig = this.buildAgentConfigForLaunch({ agentId, mode, yolo }); const globalPromptPrefix = String(this.userSettings?.global?.ui?.tasks?.launch?.globalPromptPrefix || '').trim(); const boardPromptPrefix = String(mapping?.promptPrefix || '').trim(); + const includeTicketTitle = this.userSettings?.global?.ui?.tasks?.launch?.includeTicketTitle === true; const rawPrompt = String(promptText || card?.desc || ''); const rawPromptTrimmed = String(rawPrompt || '').trim(); @@ -31385,9 +28385,9 @@ class ClaudeOrchestrator { const ticketBoardId = String(boardId || '').trim(); const ticketTitle = String(card?.name || '').trim(); - const preface = (cardUrl || ticketCardId || ticketTitle) ? [ + const preface = (cardUrl || ticketCardId || (includeTicketTitle && ticketTitle)) ? [ `Task context: this work is for a ticket.`, - ticketTitle ? `Ticket title: ${ticketTitle}` : '', + (includeTicketTitle && ticketTitle) ? `Ticket title: ${ticketTitle}` : '', cardUrl ? `Trello card: ${cardUrl}` : '', ticketCardId ? `Ticket id: trello:${ticketCardId}` : '', ``, @@ -31401,7 +28401,7 @@ class ClaudeOrchestrator { preface || '', rawPromptTrimmed || '' ].map((s) => String(s || '').replace(/\s+$/, '')).filter(Boolean).join('\n\n').trim(); - const autoSendPromptEffective = !!autoSendPrompt && !!prompt; + const autoSendPromptEffective = !!autoSendPrompt && !!rawPromptTrimmed; const recommended = this.getRecommendedWorktree(repo); @@ -31572,10 +28572,6 @@ class ClaudeOrchestrator { .replace(/\"/g, '"') .replace(/'/g, '''); } - - escapeOnclickArg(value) { - return this.escapeHtml(JSON.stringify(value)); - } } // Initialize when DOM is ready @@ -31583,10 +28579,4 @@ let orchestrator; document.addEventListener('DOMContentLoaded', () => { orchestrator = new ClaudeOrchestrator(); window.orchestrator = orchestrator; // Make globally available - try { - if (window.ProjectsBoardUI && !window.projectsBoardUI) { - window.projectsBoardUI = new window.ProjectsBoardUI(orchestrator); - orchestrator.projectsBoardUI = window.projectsBoardUI; - } - } catch {} }); diff --git a/client/commander-panel.js b/client/commander-panel.js index 2d5f6b6a..b710db78 100644 --- a/client/commander-panel.js +++ b/client/commander-panel.js @@ -60,24 +60,24 @@ class CommanderPanel {
- - -
- + +
+ - - - -
@@ -105,8 +105,6 @@ class CommanderPanel {
Loading…
`; document.body.appendChild(advice); - - this.orchestrator?.applyUiVisibility?.(); } /** diff --git a/client/dashboard.js b/client/dashboard.js index 829278c7..727e4c65 100644 --- a/client/dashboard.js +++ b/client/dashboard.js @@ -8,7 +8,6 @@ class Dashboard { this.isVisible = false; this.quickLinks = null; this._escHandler = null; - this._projectLaunchInFlight = false; } async show() { @@ -20,9 +19,8 @@ class Dashboard { window.quickLinks = this.quickLinks; // Make available globally for onclick handlers } - const visibility = this.orchestrator.getUiVisibilityConfig()?.dashboard || {}; // Fetch quick links data - re-render when complete - if (this.quickLinks && visibility.quickLinks !== false) { + if (this.quickLinks) { this.quickLinks.fetchData().then(() => { // Re-render to show quick links once loaded if (this.isVisible) { @@ -90,235 +88,138 @@ class Dashboard { this.setupEventListeners(); // Set up quick links drag and drop - const visibility = this.orchestrator.getUiVisibilityConfig()?.dashboard || {}; - if (this.quickLinks && visibility.quickLinks !== false) { + if (this.quickLinks) { this.quickLinks.setupDragAndDrop(); } // Load ports for dashboard - if (visibility.runningServices !== false) { - this.loadDashboardPorts(); - } + this.loadDashboardPorts(); // Load process status/telemetry/advice summaries - if (visibility.processSection !== false) { - this.loadDashboardProcessSummary(); - } + this.loadDashboardProcessSummary(); } generateDashboardHTML() { - const sortByLastAccess = (a, b) => { - const aTime = a.lastAccess ? new Date(a.lastAccess).getTime() : 0; - const bTime = b.lastAccess ? new Date(b.lastAccess).getTime() : 0; - return bTime - aTime; - }; - const activeWorkspaces = this.workspaces.filter(ws => this.isWorkspaceActive(ws)).sort(sortByLastAccess); - const inactiveWorkspaces = this.workspaces.filter(ws => !this.isWorkspaceActive(ws)).sort(sortByLastAccess); + const activeWorkspaces = this.workspaces.filter(ws => this.isWorkspaceActive(ws)); + const inactiveWorkspaces = this.workspaces.filter(ws => !this.isWorkspaceActive(ws)); const canReturnToWorkspaces = !!(this.orchestrator.tabManager?.tabs?.size); - const visibility = this.orchestrator.getUiVisibilityConfig()?.dashboard || {}; - const showProcessBanner = visibility.processBanner !== false; - const showProcessSection = visibility.processSection !== false; - const showStatusCard = visibility.statusCard !== false; - const showTelemetryCard = visibility.telemetryCard !== false; - const showPolecatsCard = visibility.polecatsCard !== false; - const showDiscordCard = visibility.discordCard !== false; - const showProjectsCard = visibility.projectsCard !== false; - const showAdviceCard = visibility.adviceCard !== false; - const showReadinessCard = visibility.readinessCard !== false; - - const processCards = [ - showStatusCard ? ` -
-
Status
-
Loading…
-
- ` : '', - showTelemetryCard ? ` -
-
Telemetry
-
Loading…
-
- - - - - - - - -
-
- ` : '', - showPolecatsCard ? ` -
-
Polecats
-
Loading…
-
- -
-
- ` : '', - showDiscordCard ? ` -
-
Discord
-
Loading…
-
- - - - -
-
- ` : '', - showProjectsCard ? ` -
-
Projects
-
Loading…
-
- - - -
-
- ` : '', - showAdviceCard ? ` -
-
Advice
-
Loading…
-
- - - - - - -
-
- ` : '', - showReadinessCard ? ` -
-
Readiness
-
Loading…
-
- -
-
- ` : '' - ].filter(Boolean).join(''); - - const processSection = (showProcessSection && processCards) ? ` -
-

Process

-
- ${processCards} -
-
- ` : ''; - - const activeSection = (visibility.workspacesActive !== false && activeWorkspaces.length > 0) ? ` -
-

Active Workspaces

-
- ${activeWorkspaces.map(ws => this.generateWorkspaceCard(ws, true)).join('')} -
-
- ` : ''; - - const allSection = (visibility.workspacesAll !== false) ? ` -
-

All Workspaces

-
- ${inactiveWorkspaces.map(ws => this.generateWorkspaceCard(ws, false)).join('')} -
-
- ` : ''; - - const createSection = (visibility.createSection !== false) ? ` -
-

Create

-
- - - - - -
-
- ` : ''; - - const reviewSection = (visibility.reviewSection !== false) ? ` -
-

Review

-
- - -
-
- ` : ''; - - const quickLinksSection = (visibility.quickLinks !== false) ? ` -
-

Quick Links

- -
- ` : ''; - - const runningServicesSection = (visibility.runningServices !== false) ? ` -
-

Running Services

-
-
Loading services...
-
-
- ` : ''; return `
${canReturnToWorkspaces ? ` ` : `
`} - ${showProcessBanner ? `
` : `
`} +
-

Dashboard

+

🎯 Agent Orchestrator Dashboard

Select a workspace to begin development

- ${processSection} - -
-
- ${activeSection} - ${allSection} -
-
- ${createSection} - ${reviewSection} - ${quickLinksSection} - ${runningServicesSection} +
+

πŸ“Š Process

+
+
+
Status
+
Loading…
+
+
+
Telemetry
+
Loading…
+
+ + + + + + + + +
+
+
+
Polecats
+
Loading…
+
+ +
+
+
+
Discord
+
Loading…
+
+ + + +
+
+
+
Projects
+
Loading…
+
+ + +
+
+
+
Advice
+
Loading…
+
+ + + + + + +
+
+
+
Readiness
+
Loading…
+
+ +
+
+ + ${activeWorkspaces.length > 0 ? ` +
+

Active Workspaces

+
+ ${activeWorkspaces.map(ws => this.generateWorkspaceCard(ws, true)).join('')} +
+
+ ` : ''} + +
+

All Workspaces

+
+ ${inactiveWorkspaces.map(ws => this.generateWorkspaceCard(ws, false)).join('')} + ${this.generateCreateWorkspaceCard()} +
+
+ +
+
+

πŸ”— Quick Links

+ +
+ +
+

πŸ”Œ Running Services

+
+
Loading services...
+
+
+
`; } - async loadDashboardProcessSummary() { - const visibility = this.orchestrator.getUiVisibilityConfig()?.dashboard || {}; - const showStatus = visibility.statusCard !== false; - const showTelemetry = visibility.telemetryCard !== false; - const showPolecats = visibility.polecatsCard !== false; - const showDiscord = visibility.discordCard !== false; - const showProjects = visibility.projectsCard !== false; - const showAdvice = visibility.adviceCard !== false; - const showReadiness = visibility.readinessCard !== false; - const showAny = showStatus || showTelemetry || showPolecats || showDiscord || showProjects || showAdvice || showReadiness; - if (!showAny) return; - - const statusEl = document.getElementById('dashboard-status-summary'); + async loadDashboardProcessSummary() { + const statusEl = document.getElementById('dashboard-status-summary'); const telemetryEl = document.getElementById('dashboard-telemetry-summary'); const polecatsEl = document.getElementById('dashboard-polecats-summary'); const discordEl = document.getElementById('dashboard-discord-summary'); @@ -350,26 +251,6 @@ class Dashboard { e.preventDefault(); this.showPolecatOverlay().catch(() => {}); }); - // Discord auto-start checkbox - const discordAutostartCb = document.getElementById('dashboard-discord-autostart'); - if (discordAutostartCb) { - // Load current setting - try { - const res = await fetch('/api/user-settings').catch(() => null); - const settings = res ? await res.json().catch(() => ({})) : {}; - discordAutostartCb.checked = settings?.global?.ui?.discord?.autoEnsureServicesAtStartup === true; - } catch { /* leave unchecked */ } - - discordAutostartCb.addEventListener('change', async (e) => { - const enabled = !!e.target.checked; - await this.orchestrator?.updateGlobalUserSetting?.('ui.discord.autoEnsureServicesAtStartup', enabled); - if (enabled) { - await this.ensureDiscordServices(); - await this.loadDashboardDiscordSummary(discordEl); - } - }); - } - document.getElementById('dashboard-discord-ensure')?.addEventListener('click', async (e) => { e.preventDefault(); await this.ensureDiscordServices(); @@ -424,12 +305,6 @@ class Dashboard { this.showProjectHealthOverlay(); } catch {} }); - document.getElementById('dashboard-open-project-board')?.addEventListener('click', (e) => { - e.preventDefault(); - try { - this.orchestrator?.projectsBoardUI?.show?.(); - } catch {} - }); document.getElementById('dashboard-open-advice')?.addEventListener('click', (e) => { e.preventDefault(); try { @@ -455,12 +330,10 @@ class Dashboard { } catch {} }); - if (showPolecats) { - try { - this.updatePolecatSummary(polecatsEl); - } catch { - // ignore - } + try { + this.updatePolecatSummary(polecatsEl); + } catch { + // ignore } const escapeHtml = (value) => String(value ?? '') @@ -468,7 +341,7 @@ class Dashboard { .replace(//g, '>'); - const renderAdvice = async ({ force = false } = {}) => { + const renderAdvice = async ({ force = false } = {}) => { if (!adviceEl) return; adviceEl.textContent = 'Loading…'; @@ -527,24 +400,15 @@ class Dashboard { }); }; - try { - const projectsBoardPromise = (showProjects && this.orchestrator?.getProjectsBoard) - ? this.orchestrator.getProjectsBoard({ force: false }).catch(() => null) - : Promise.resolve(null); - const scannedReposPromise = (showProjects && this.orchestrator?.getScannedRepos) - ? this.orchestrator.getScannedRepos({ force: false }).catch(() => []) - : Promise.resolve([]); - - const [statusRes, telemetryRes, projectsRes, readinessRes, projectsBoardData, scannedRepos] = await Promise.all([ - showStatus ? fetch('/api/process/status?mode=mine').catch(() => null) : Promise.resolve(null), - showTelemetry ? fetch('/api/process/telemetry').catch(() => null) : Promise.resolve(null), - showProjects ? fetch('/api/process/projects?mode=mine').catch(() => null) : Promise.resolve(null), - showReadiness ? fetch('/api/process/readiness/templates').catch(() => null) : Promise.resolve(null), - projectsBoardPromise, - scannedReposPromise - ]); - - if (showStatus && statusEl) { + try { + const [statusRes, telemetryRes, projectsRes, readinessRes] = await Promise.all([ + fetch('/api/process/status?mode=mine').catch(() => null), + fetch('/api/process/telemetry').catch(() => null), + fetch('/api/process/projects?mode=mine').catch(() => null), + fetch('/api/process/readiness/templates').catch(() => null) + ]); + + if (statusEl) { const data = statusRes ? await statusRes.json().catch(() => ({})) : {}; if (statusRes && statusRes.ok) { const q = data?.qByTier || {}; @@ -558,7 +422,7 @@ class Dashboard { } } - if (showTelemetry && telemetryEl) { + if (telemetryEl) { const data = telemetryRes ? await telemetryRes.json().catch(() => ({})) : {}; if (telemetryRes && telemetryRes.ok) { this._telemetrySummary = data; @@ -581,213 +445,70 @@ class Dashboard { } } - if (showProjects && projectsEl) { - const data = projectsRes ? await projectsRes.json().catch(() => ({})) : {}; - const projectsBoard = projectsBoardData?.board && typeof projectsBoardData.board === 'object' ? projectsBoardData.board : null; - const scanned = Array.isArray(scannedRepos) ? scannedRepos : []; - - const normalizeKey = (value) => (this.orchestrator?.normalizeProjectsBoardProjectKey?.(value) ?? String(value || '').trim().replace(/\\/g, '/')); - - const boardHtml = (() => { - if (!projectsBoard || scanned.length === 0) return ''; - - const repoByKey = new Map(); - for (const repo of scanned) { - const key = normalizeKey(repo?.relativePath); - if (!key) continue; - if (!repoByKey.has(key)) repoByKey.set(key, repo); - } - if (!repoByKey.size) return ''; - - const getOrderIndex = (columnId) => { - const raw = projectsBoard?.orderByColumn && typeof projectsBoard.orderByColumn === 'object' - ? projectsBoard.orderByColumn[columnId] - : null; - const order = Array.isArray(raw) ? raw : []; - const index = new Map(); - order.forEach((k, i) => { - const key = normalizeKey(k); - if (!key || index.has(key)) return; - index.set(key, i); - }); - return index; - }; - - const collect = (columnId) => { - const out = []; - for (const [key, repo] of repoByKey.entries()) { - const col = this.orchestrator?.getProjectsBoardColumnForProjectKey?.(key, projectsBoardData) || 'backlog'; - if (col === columnId) out.push({ key, repo }); - } - const index = getOrderIndex(columnId); - out.sort((a, b) => { - const aRank = index.has(a.key) ? index.get(a.key) : Number.POSITIVE_INFINITY; - const bRank = index.has(b.key) ? index.get(b.key) : Number.POSITIVE_INFINITY; - if (aRank !== bRank) return aRank - bRank; - return String(a.repo?.name || '').localeCompare(String(b.repo?.name || '')); - }); - return out; - }; - - const shipNext = collect('next'); - const active = collect('active'); - const total = shipNext.length + active.length; - if (total === 0) return ''; - - const tagMap = projectsBoard?.tagsByProjectKey && typeof projectsBoard.tagsByProjectKey === 'object' - ? projectsBoard.tagsByProjectKey - : {}; - - const renderTile = (item) => { - const icon = this.orchestrator?.getProjectIcon?.(item?.repo?.type) || 'πŸ“'; - const name = String(item?.repo?.name || item?.key || '').trim(); - const key = normalizeKey(item?.key); - const category = String(item?.repo?.category || '').trim(); - const type = String(item?.repo?.type || '').trim(); - const subtitle = category ? `${category} β€’ ${key}` : key; - const isLive = !!tagMap[key]?.live; - return ` - - `; - }; - - const renderGroup = (label, list) => { - if (!list.length) return ''; - return ` -
-
${escapeHtml(label)} ${list.length}
-
- ${list.map(renderTile).join('')} -
-
- `; - }; + if (projectsEl) { + const data = projectsRes ? await projectsRes.json().catch(() => ({})) : {}; + if (projectsRes && projectsRes.ok) { + const totals = data?.totals || {}; + const repos = Array.isArray(data?.repos) ? data.repos : []; + const top = repos.slice(0, 6); + + const pickWorstRisk = (counts) => { + const c = counts && typeof counts === 'object' ? counts : {}; + if (Number(c.critical || 0) > 0) return 'critical'; + if (Number(c.high || 0) > 0) return 'high'; + if (Number(c.medium || 0) > 0) return 'medium'; + if (Number(c.low || 0) > 0) return 'low'; + return ''; + }; - return ` -
- ${renderGroup('Ship Next', shipNext)} - ${renderGroup('Active', active)} -
- `; - })(); - - const prSummaryHtml = (() => { - if (!(projectsRes && projectsRes.ok)) { - return `
Failed to load PR summary.
`; - } - - const totals = data?.totals || {}; - const repos = Array.isArray(data?.repos) ? data.repos : []; - const top = repos.slice(0, 6); - - const pickWorstRisk = (counts) => { - const c = counts && typeof counts === 'object' ? counts : {}; - if (Number(c.critical || 0) > 0) return 'critical'; - if (Number(c.high || 0) > 0) return 'high'; - if (Number(c.medium || 0) > 0) return 'medium'; - if (Number(c.low || 0) > 0) return 'low'; - return ''; - }; - - const riskChip = (risk) => { - const r = String(risk || '').trim().toLowerCase(); - if (!r) return ''; - const cls = (r === 'critical' || r === 'high') ? 'level-warn' : ''; - return `${escapeHtml(r)}`; - }; + const riskChip = (risk) => { + const r = String(risk || '').trim().toLowerCase(); + if (!r) return ''; + const cls = (r === 'critical' || r === 'high') ? 'level-warn' : ''; + return `${escapeHtml(r)}`; + }; - return ` -
Repos ${Number(totals?.repos ?? top.length ?? 0)} β€’ Open PRs ${Number(totals?.prsOpen ?? 0)}
-
Unreviewed ${Number(totals?.prsUnreviewed ?? 0)} β€’ Needs fix ${Number(totals?.prsNeedsFix ?? 0)}
-
- ${top.length ? top.map((r) => { - const repo = String(r?.repo || '').trim(); - const open = Number(r?.prsOpen ?? 0); - const unrev = Number(r?.prsUnreviewed ?? 0); - const avgReview = r?.telemetry?.avgReviewSeconds ? `${Math.round(Number(r.telemetry.avgReviewSeconds))}s` : 'β€”'; - const worstRisk = pickWorstRisk(r?.riskCounts); - return ` - - `; - }).join('') : `
No PRs found.
`} -
- `; - })(); - - projectsEl.innerHTML = `${boardHtml}${prSummaryHtml}`; - - projectsEl.querySelectorAll('[data-dashboard-start-project]').forEach((btn) => { - btn.addEventListener('click', async () => { - if (this._projectLaunchInFlight) return; - const key = String(btn.getAttribute('data-dashboard-start-project') || '').trim(); - if (!key) return; - this._projectLaunchInFlight = true; - btn.disabled = true; - try { - const currentId = String(this.orchestrator?.currentWorkspace?.id || '').trim(); - const workspaces = Array.isArray(this.workspaces) ? this.workspaces : []; - const pickRecent = () => { - if (currentId) return currentId; - let best = null; - let bestTime = 0; - for (const ws of workspaces) { - const t = ws?.lastAccess ? new Date(ws.lastAccess).getTime() : 0; - if (!best || t > bestTime) { - best = ws; - bestTime = t; - } - } - return String(best?.id || '').trim(); - }; - - const targetId = pickRecent(); - try { this.orchestrator?.hideDashboard?.(); } catch {} - if (targetId && targetId !== currentId) { - this.orchestrator?.switchToWorkspace?.(targetId); - await this.orchestrator?.waitForWorkspaceActive?.(targetId).catch(() => false); - } - await this.orchestrator?.startProjectWorktreeFromBoardKey?.(key); - } catch { - this.orchestrator?.showToast?.('Failed to start worktree', 'error'); - } finally { - btn.disabled = false; - this._projectLaunchInFlight = false; - } - }); - }); + projectsEl.innerHTML = ` +
Repos ${Number(totals?.repos ?? top.length ?? 0)} β€’ Open PRs ${Number(totals?.prsOpen ?? 0)}
+
Unreviewed ${Number(totals?.prsUnreviewed ?? 0)} β€’ Needs fix ${Number(totals?.prsNeedsFix ?? 0)}
+
+ ${top.length ? top.map((r) => { + const repo = String(r?.repo || '').trim(); + const open = Number(r?.prsOpen ?? 0); + const unrev = Number(r?.prsUnreviewed ?? 0); + const avgReview = r?.telemetry?.avgReviewSeconds ? `${Math.round(Number(r.telemetry.avgReviewSeconds))}s` : 'β€”'; + const worstRisk = pickWorstRisk(r?.riskCounts); + return ` + + `; + }).join('') : `
No PRs found.
`} +
+ `; - projectsEl.querySelectorAll('[data-open-repo]').forEach((btn) => { - btn.addEventListener('click', () => { - const repo = btn.getAttribute('data-open-repo') || ''; - if (!repo) return; - try { - localStorage.setItem('prs-panel-repo', repo); - } catch {} - try { - this.orchestrator?.showPRsPanel?.(); - } catch {} - }); - }); - } + projectsEl.querySelectorAll('[data-open-repo]').forEach((btn) => { + btn.addEventListener('click', () => { + const repo = btn.getAttribute('data-open-repo') || ''; + if (!repo) return; + try { + localStorage.setItem('prs-panel-repo', repo); + } catch {} + try { + this.orchestrator?.showPRsPanel?.(); + } catch {} + }); + }); + } else { + projectsEl.textContent = 'Failed to load.'; + } + } - if (showReadiness && readinessEl) { + if (readinessEl) { const data = readinessRes ? await readinessRes.json().catch(() => ({})) : {}; if (readinessRes && readinessRes.ok) { const templates = Array.isArray(data?.templates) ? data.templates : []; @@ -801,19 +522,15 @@ class Dashboard { } } - if (showDiscord) { - await this.loadDashboardDiscordSummary(discordEl); - } - if (showAdvice) { - await renderAdvice({ force: false }); - } + await this.loadDashboardDiscordSummary(discordEl); + await renderAdvice({ force: false }); } catch (error) { - if (showStatus && statusEl) statusEl.textContent = 'Failed to load.'; - if (showTelemetry && telemetryEl) telemetryEl.textContent = 'Failed to load.'; - if (showProjects && projectsEl) projectsEl.textContent = 'Failed to load.'; - if (showReadiness && readinessEl) readinessEl.textContent = 'Failed to load.'; - if (showDiscord && discordEl) discordEl.textContent = 'Failed to load.'; - if (showAdvice) await renderAdvice({ force: false }); + if (statusEl) statusEl.textContent = 'Failed to load.'; + if (telemetryEl) telemetryEl.textContent = 'Failed to load.'; + if (projectsEl) projectsEl.textContent = 'Failed to load.'; + if (readinessEl) readinessEl.textContent = 'Failed to load.'; + if (discordEl) discordEl.textContent = 'Failed to load.'; + await renderAdvice({ force: false }); } } @@ -1033,10 +750,7 @@ class Dashboard { - -
-
Loading…
@@ -1069,20 +783,9 @@ class Dashboard { const bucket = Number(bucketEl?.value ?? 60); await this.createTelemetrySnapshotLink({ lookbackHours: hours, bucketMinutes: bucket }); }); - overlay.querySelector('#dashboard-telemetry-benchmark')?.addEventListener('click', async () => { - const hours = Number(lookbackEl?.value ?? 24); - const bucket = Number(bucketEl?.value ?? 60); - await this.createTelemetryBenchmarkSnapshot({ lookbackHours: hours, bucketMinutes: bucket }); - }); - overlay.querySelector('#dashboard-telemetry-release-notes')?.addEventListener('click', async () => { - const hours = Number(lookbackEl?.value ?? 24); - const bucket = Number(bucketEl?.value ?? 60); - await this.copyTelemetryReleaseNotes({ lookbackHours: hours, bucketMinutes: bucket }); - }); overlay.querySelector('#dashboard-telemetry-refresh')?.addEventListener('click', () => { this.loadTelemetryDetails({ lookbackHours: Number(lookbackEl?.value ?? 24), bucketMinutes: Number(bucketEl?.value ?? 60) }); }); - this.loadTelemetryPluginActions(overlay).catch(() => {}); const onKey = (e) => { if (e.key !== 'Escape') return; @@ -3068,24 +2771,16 @@ class Dashboard { const bucket = Number(bucketMinutes); const safeHours = Number.isFinite(hours) && hours > 0 ? hours : 24; const safeBucket = Number.isFinite(bucket) && bucket > 0 ? bucket : 60; - const detailsUrl = `/api/process/telemetry/details?lookbackHours=${encodeURIComponent(String(safeHours))}&bucketMinutes=${encodeURIComponent(String(safeBucket))}`; - const benchmarkUrl = `/api/process/telemetry/benchmarks?lookbackHours=${encodeURIComponent(String(safeHours))}&bucketMinutes=${encodeURIComponent(String(safeBucket))}&limit=8`; + const url = `/api/process/telemetry/details?lookbackHours=${encodeURIComponent(String(safeHours))}&bucketMinutes=${encodeURIComponent(String(safeBucket))}`; try { - const [detailsRes, benchmarkRes] = await Promise.all([ - fetch(detailsUrl).catch(() => null), - fetch(benchmarkUrl).catch(() => null) - ]); - - const data = detailsRes ? await detailsRes.json().catch(() => ({})) : {}; - if (!detailsRes || !detailsRes.ok) { + const res = await fetch(url).catch(() => null); + const data = res ? await res.json().catch(() => ({})) : {}; + if (!res || !res.ok) { body.textContent = 'Failed to load.'; return; } - const benchmark = benchmarkRes && benchmarkRes.ok - ? await benchmarkRes.json().catch(() => null) - : null; - body.innerHTML = this.renderTelemetryDetails(data, benchmark); + body.innerHTML = this.renderTelemetryDetails(data); } catch { body.textContent = 'Failed to load.'; } @@ -3115,53 +2810,6 @@ class Dashboard { } } - async createTelemetryBenchmarkSnapshot({ lookbackHours = 24, bucketMinutes = 60 } = {}) { - const hours = Number(lookbackHours); - const bucket = Number(bucketMinutes); - const safeHours = Number.isFinite(hours) && hours > 0 ? hours : 24; - const safeBucket = Number.isFinite(bucket) && bucket > 0 ? bucket : 60; - const defaultLabel = `release ${new Date().toISOString().slice(0, 10)}`; - const label = window.prompt('Benchmark label (release/tag)', defaultLabel); - if (label === null) return; - - try { - const res = await fetch('/api/process/telemetry/benchmarks/snapshots', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - label, - lookbackHours: safeHours, - bucketMinutes: safeBucket - }) - }).catch(() => null); - const data = res ? await res.json().catch(() => ({})) : {}; - if (!res || !res.ok) throw new Error(data?.error || 'Failed to create benchmark snapshot'); - try { this.orchestrator?.showToast?.(`Benchmark saved: ${String(data?.label || 'snapshot')}`, 'success'); } catch {} - this.loadTelemetryDetails({ lookbackHours: safeHours, bucketMinutes: safeBucket }); - } catch (e) { - try { this.orchestrator?.showToast?.(String(e?.message || e), 'error'); } catch {} - } - } - - async copyTelemetryReleaseNotes({ lookbackHours = 24, bucketMinutes = 60 } = {}) { - const hours = Number(lookbackHours); - const bucket = Number(bucketMinutes); - const safeHours = Number.isFinite(hours) && hours > 0 ? hours : 24; - const safeBucket = Number.isFinite(bucket) && bucket > 0 ? bucket : 60; - const url = `/api/process/telemetry/benchmarks/release-notes?currentId=live&lookbackHours=${encodeURIComponent(String(safeHours))}&bucketMinutes=${encodeURIComponent(String(safeBucket))}`; - try { - const res = await fetch(url).catch(() => null); - const data = res ? await res.json().catch(() => ({})) : {}; - if (!res || !res.ok) throw new Error(data?.error || 'Failed to build release notes'); - const markdown = String(data?.markdown || '').trim(); - if (!markdown) throw new Error('Release notes were empty'); - await this.copyToClipboard(markdown); - try { this.orchestrator?.showToast?.('Release notes copied', 'success'); } catch {} - } catch (e) { - try { this.orchestrator?.showToast?.(String(e?.message || e), 'error'); } catch {} - } - } - async copyToClipboard(text) { const t = String(text || ''); if (!t) return; @@ -3180,40 +2828,7 @@ class Dashboard { } } - async loadTelemetryPluginActions(overlay) { - const holder = overlay?.querySelector?.('#dashboard-telemetry-plugin-actions'); - if (!holder) return; - holder.innerHTML = ''; - const host = window.orchestratorPluginHost; - if (!host || typeof host.refresh !== 'function') return; - try { - await host.refresh({ slot: 'dashboard.telemetry.actions', force: true }); - const items = host.getSlotItems('dashboard.telemetry.actions'); - if (!items.length) return; - for (const item of items) { - const btn = document.createElement('button'); - btn.type = 'button'; - btn.className = 'btn-secondary'; - btn.textContent = String(item?.label || item?.id || 'Plugin'); - const desc = String(item?.description || '').trim(); - if (desc) btn.title = desc; - btn.addEventListener('click', async () => { - try { - const result = await host.runAction(item, { orchestrator: this.orchestrator }); - if (result?.ok) return; - this.orchestrator?.showToast?.(String(result?.error || 'Plugin action failed'), 'error'); - } catch (error) { - this.orchestrator?.showToast?.(String(error?.message || error), 'error'); - } - }); - holder.appendChild(btn); - } - } catch { - // ignore plugin slot errors in dashboard - } - } - - renderTelemetryDetails(data, benchmarkData = null) { + renderTelemetryDetails(data) { const escapeHtml = (value) => String(value ?? '') .replace(/&/g, '&') .replace(/ @@ -3321,70 +2935,6 @@ class Dashboard { ${histogram(promptHist, { formatLabel: (v) => `${Math.round(Number(v) || 0)}` })} - ${benchmarkSection} - `; - } - - renderTelemetryBenchmark(data) { - const escapeHtml = (value) => String(value ?? '') - .replace(/&/g, '&') - .replace(//g, '>'); - const rows = Array.isArray(data?.rows) ? data.rows : []; - if (!rows.length) { - return ` -
-
Release benchmark
-
No benchmark snapshots captured yet.
-
- `; - } - - const formatSeconds = (value) => { - const n = Number(value); - if (!Number.isFinite(n) || n <= 0) return 'β€”'; - if (n < 60) return `${Math.round(n)}s`; - if (n < 3600) return `${Math.round(n / 60)}m`; - return `${(n / 3600).toFixed(1)}h`; - }; - const sign = (value) => { - const n = Number(value); - if (!Number.isFinite(n)) return 'β€”'; - return n > 0 ? `+${Math.round(n)}` : `${Math.round(n)}`; - }; - - const listHtml = rows.slice(0, 6).map((row) => { - const metrics = row?.metrics || {}; - const onboarding = Number(metrics?.onboarding?.score ?? 0); - const runtime = Number(metrics?.runtime?.score ?? 0); - const review = Number(metrics?.review?.score ?? 0); - const cycle = metrics?.review?.avgReviewSeconds; - const done = Number(metrics?.review?.doneCount ?? 0); - const merged = Number(metrics?.review?.prMergedCount ?? 0); - const delta = row?.deltaFromPrevious || null; - const deltaText = delta - ? `Ξ” onboarding ${sign(delta.onboardingScore)} β€’ runtime ${sign(delta.runtimeScore)} β€’ review ${sign(delta.reviewScore)}` - : 'Ξ” baseline n/a'; - return ` -
-
-
${escapeHtml(String(row?.label || row?.id || 'snapshot'))} (${escapeHtml(String(row?.createdAt || ''))})
-
${escapeHtml(deltaText)}
-
-
-
onboarding ${onboarding} β€’ runtime ${runtime} β€’ review ${review}
-
cycle ${escapeHtml(formatSeconds(cycle))} β€’ done ${done} β€’ merged ${merged}
-
-
- `; - }).join(''); - - return ` -
-
Release benchmark snapshots
-
Track onboarding, runtime and review metrics across releases.
- ${listHtml} -
`; } @@ -3491,28 +3041,6 @@ class Dashboard { `; } - generateCreateProjectCard() { - return ` -
-
- ✨ -
-

New Project

-

Greenfield Flow

-
-
- -
-

Create a brand-new project, scaffold it, and open a workspace in one flow

-
- - -
- `; - } - generateQuickLinksHTML() { // Get globalShortcuts from config const globalShortcuts = this.orchestrator.orchestratorConfig?.globalShortcuts || []; @@ -3621,35 +3149,6 @@ class Dashboard { }); } - const createProjectBtn = document.querySelector('.workspace-create-project-btn'); - if (createProjectBtn) { - createProjectBtn.addEventListener('click', () => { - this.showCreateProjectWizard(); - }); - } - - const reviewInboxBtn = document.getElementById('dashboard-review-inbox'); - if (reviewInboxBtn) { - reviewInboxBtn.addEventListener('click', () => { - this.orchestrator.openReviewInbox(); - }); - } - - const quickReviewBtn = document.getElementById('dashboard-quick-review'); - if (quickReviewBtn) { - quickReviewBtn.addEventListener('click', () => { - this.orchestrator.openReviewInbox({ quick: true }); - }); - } - - const commanderToggleBtn = document.getElementById('dashboard-commander-toggle'); - if (commanderToggleBtn) { - commanderToggleBtn.addEventListener('click', () => { - this.toggleCommanderFromDashboard(); - }); - this.updateCommanderToggle(); - } - // ESC: return to tabbed workspaces if dashboard was opened from there if (this._escHandler) { document.removeEventListener('keydown', this._escHandler); @@ -3696,16 +3195,6 @@ class Dashboard { } } - showCreateProjectWizard() { - if (typeof this.orchestrator?.openGreenfieldWizard === 'function') { - this.orchestrator.openGreenfieldWizard().catch((error) => { - this.orchestrator?.showToast?.(`Failed to open New Project wizard: ${String(error?.message || error)}`, 'error'); - }); - return; - } - this.orchestrator?.showToast?.('New Project wizard is unavailable in this build', 'warning'); - } - async openWorkspace(workspaceId) { console.log('Opening workspace:', workspaceId); @@ -3811,20 +3300,17 @@ class Dashboard { async createEmptyWorkspaceQuick() { try { + const timestamp = new Date(); + const stamp = timestamp.toISOString().replace(/[:T]/g, '-').slice(0, 19); + const name = `Empty Workspace ${timestamp.toLocaleString()}`; + const baseId = `empty-${stamp}`; + const randomSuffix = Math.random().toString(36).slice(2, 6); + let workspaceId = `${baseId}-${randomSuffix}`; + + // Ensure ID is unique against current list const existingIds = new Set(this.workspaces.map(ws => ws.id)); - const existingNumbers = this.workspaces - .map(ws => { - const match = String(ws?.name || '').match(/^Workspace\s+(\d+)$/i); - return match ? Number(match[1]) : NaN; - }) - .filter(n => Number.isFinite(n)); - let nextNumber = existingNumbers.length ? Math.max(...existingNumbers) + 1 : 1; - let name = `Workspace ${nextNumber}`; - let workspaceId = `workspace-${nextNumber}`; - while (existingIds.has(workspaceId)) { - nextNumber += 1; - name = `Workspace ${nextNumber}`; - workspaceId = `workspace-${nextNumber}`; + if (existingIds.has(workspaceId)) { + workspaceId = `${baseId}-${Math.random().toString(36).slice(2, 8)}`; } const workspaceConfig = { @@ -3832,7 +3318,7 @@ class Dashboard { name, type: 'custom', icon: '🧱', - description: 'Workspace (add worktrees later)', + description: 'Empty workspace (add worktrees later)', access: 'private', empty: true, repository: { @@ -3900,31 +3386,6 @@ class Dashboard { } } - async updateCommanderToggle() { - const btn = document.getElementById('dashboard-commander-toggle'); - if (!btn) return; - try { - const res = await fetch('/api/commander/status').catch(() => null); - const data = res ? await res.json().catch(() => ({})) : {}; - const running = !!data?.running; - btn.dataset.running = running ? 'true' : 'false'; - btn.textContent = running ? 'Commander: On' : 'Commander: Off'; - } catch { - btn.dataset.running = 'false'; - btn.textContent = 'Commander: Off'; - } - } - - async toggleCommanderFromDashboard() { - const btn = document.getElementById('dashboard-commander-toggle'); - const running = btn?.dataset?.running === 'true'; - const endpoint = running ? '/api/commander/stop' : '/api/commander/start'; - try { - await fetch(endpoint, { method: 'POST' }).catch(() => null); - } catch {} - await this.updateCommanderToggle(); - } - async downloadWorkspaceExport(workspaceId) { const id = String(workspaceId || '').trim(); if (!id) return; @@ -4215,24 +3676,6 @@ class Dashboard { let sessions = recoveryInfo.sessions || []; let savedAt = recoveryInfo.savedAt ? new Date(recoveryInfo.savedAt).toLocaleString() : 'Unknown'; let savedAtRaw = String(recoveryInfo.savedAt || '').trim(); - let configuredTerminalCount = Number(recoveryInfo?.configuredTerminalCount || 0); - let configuredWorktreeCount = Number(recoveryInfo?.configuredWorktreeCount || 0); - - const renderRecoverySummaryHtml = () => { - const recoverableLabel = `${sessions.length} recoverable`; - const worktreeLabel = `${configuredWorktreeCount || 0} configured worktree${configuredWorktreeCount === 1 ? '' : 's'}`; - const terminalLabel = `${configuredTerminalCount || 0} configured terminal${configuredTerminalCount === 1 ? '' : 's'}`; - return ` -
- ${recoverableLabel} - ${worktreeLabel} - ${terminalLabel} -
-
- Last recovery snapshot: ${savedAt} -
- `; - }; const modal = document.createElement('div'); modal.id = 'recovery-dialog'; @@ -4244,10 +3687,7 @@ class Dashboard {
- ${renderRecoverySummaryHtml()} -
-
- Recoverable sessions are only terminals with resumable agent/shell state. Opening the workspace still loads all configured worktrees/terminals. + Found ${sessions.length} recoverable session${sessions.length !== 1 ? 's' : ''} from ${savedAt}
${sessions.length === 0 ? '
No sessions to recover
' : @@ -4387,11 +3827,9 @@ class Dashboard { sessions = next?.sessions || []; savedAt = next?.savedAt ? new Date(next.savedAt).toLocaleString() : savedAt; savedAtRaw = String(next?.savedAt || savedAtRaw || '').trim(); - configuredTerminalCount = Number(next?.configuredTerminalCount ?? configuredTerminalCount); - configuredWorktreeCount = Number(next?.configuredWorktreeCount ?? configuredWorktreeCount); if (infoEl) { - infoEl.innerHTML = renderRecoverySummaryHtml(); + infoEl.textContent = `Found ${sessions.length} recoverable session${sessions.length !== 1 ? 's' : ''} from ${savedAt}`; } renderSessions(); } catch (error) { diff --git a/client/greenfield-wizard.js b/client/greenfield-wizard.js index 49a56891..d2c65fbc 100644 --- a/client/greenfield-wizard.js +++ b/client/greenfield-wizard.js @@ -8,11 +8,6 @@ class GreenfieldWizard { name: '', description: '', category: '', - framework: '', - template: '', - repo: '', - githubOrg: '', - createGithub: true, detectedCategory: '', isPrivate: true, worktreeCount: 8, @@ -20,293 +15,53 @@ class GreenfieldWizard { yolo: true }; this.categories = []; - this.frameworks = []; - this.templates = []; - this.contextSuggestion = null; // Always use same-origin API requests; the dev server proxies `/api` to the backend. this.serverUrl = window.location.origin; - this._onEscape = null; } async show() { console.log('Opening greenfield project wizard...'); - // Fetch taxonomy and derive defaults before rendering. - this.contextSuggestion = null; - await this.loadTaxonomy(); - this.applyContextSuggestion(); - this.ensureValidSelection(); + // Fetch available categories + await this.loadCategories(); // Show wizard modal this.renderWizard(); this.showStep(1); } - async loadTaxonomy() { + async loadCategories() { try { - const taxonomy = await this.orchestrator?.ensureProjectTypeTaxonomy?.(); - if (taxonomy && Array.isArray(taxonomy.categories) && taxonomy.categories.length && Array.isArray(taxonomy.templates)) { - this.categories = taxonomy.categories.map((category) => ({ - id: String(category?.id || '').trim(), - name: String(category?.name || category?.id || '').trim(), - description: String(category?.description || '').trim(), - path: String(category?.basePathResolved || category?.path || category?.basePath || '').trim(), - keywords: Array.isArray(category?.keywords) ? category.keywords : [], - defaultTemplateId: String(category?.defaultTemplateId || '').trim(), - frameworkIds: Array.isArray(category?.frameworkIds) ? category.frameworkIds.map((id) => String(id || '').trim()).filter(Boolean) : [] - })).filter((item) => item.id); - this.frameworks = Array.isArray(taxonomy.frameworks) ? taxonomy.frameworks.map((framework) => ({ - id: String(framework?.id || '').trim(), - name: String(framework?.name || framework?.id || '').trim(), - description: String(framework?.description || '').trim(), - categoryId: String(framework?.categoryId || '').trim(), - defaultTemplateId: String(framework?.defaultTemplateId || '').trim(), - templateIds: Array.isArray(framework?.templateIds) ? framework.templateIds.map((id) => String(id || '').trim()).filter(Boolean) : [] - })).filter((item) => item.id) : []; - this.templates = taxonomy.templates.map((template) => ({ - id: String(template?.id || '').trim(), - name: String(template?.name || template?.id || '').trim(), - description: String(template?.description || '').trim(), - categoryId: String(template?.categoryId || '').trim(), - frameworkId: String(template?.frameworkId || '').trim(), - defaultRepositoryType: String(template?.defaultRepositoryType || '').trim() - })).filter((item) => item.id); - console.log('Loaded project taxonomy:', { - categories: this.categories.length, - frameworks: this.frameworks.length, - templates: this.templates.length - }); - return; - } - - const response = await fetch(`${this.serverUrl}/api/project-types`); + const response = await fetch(`${this.serverUrl}/api/greenfield/categories`); if (response.ok) { - const payload = await response.json(); - this.categories = Array.isArray(payload?.categories) ? payload.categories.map((category) => ({ - id: String(category?.id || '').trim(), - name: String(category?.name || category?.id || '').trim(), - description: String(category?.description || '').trim(), - path: String(category?.basePathResolved || category?.path || category?.basePath || '').trim(), - keywords: Array.isArray(category?.keywords) ? category.keywords : [], - defaultTemplateId: String(category?.defaultTemplateId || '').trim(), - frameworkIds: Array.isArray(category?.frameworkIds) ? category.frameworkIds.map((id) => String(id || '').trim()).filter(Boolean) : [] - })).filter((item) => item.id) : []; - this.frameworks = Array.isArray(payload?.frameworks) ? payload.frameworks.map((framework) => ({ - id: String(framework?.id || '').trim(), - name: String(framework?.name || framework?.id || '').trim(), - description: String(framework?.description || '').trim(), - categoryId: String(framework?.categoryId || '').trim(), - defaultTemplateId: String(framework?.defaultTemplateId || '').trim(), - templateIds: Array.isArray(framework?.templateIds) ? framework.templateIds.map((id) => String(id || '').trim()).filter(Boolean) : [] - })).filter((item) => item.id) : []; - this.templates = Array.isArray(payload?.templates) ? payload.templates.map((template) => ({ - id: String(template?.id || '').trim(), - name: String(template?.name || template?.id || '').trim(), - description: String(template?.description || '').trim(), - categoryId: String(template?.categoryId || '').trim(), - frameworkId: String(template?.frameworkId || '').trim(), - defaultRepositoryType: String(template?.defaultRepositoryType || '').trim() - })).filter((item) => item.id) : []; - return; + this.categories = await response.json(); + console.log('Loaded categories:', this.categories); } } catch (error) { - console.error('Failed to load project taxonomy:', error); - } - - this.categories = [ - { id: 'website', name: 'Website', path: '~/GitHub/websites', keywords: ['website'], defaultTemplateId: 'website-starter', frameworkIds: ['web-generic'] }, - { id: 'game', name: 'Game', path: '~/GitHub/games', keywords: ['game'], defaultTemplateId: 'hytopia-game-starter', frameworkIds: ['hytopia', 'monogame'] }, - { id: 'tool', name: 'Tool', path: '~/GitHub/tools', keywords: ['tool'], defaultTemplateId: 'node-typescript-tool', frameworkIds: ['nodejs'] }, - { id: 'other', name: 'Other', path: '~/GitHub/projects', keywords: [], defaultTemplateId: 'generic-empty', frameworkIds: ['generic'] } - ]; - this.frameworks = [ - { id: 'web-generic', name: 'Web', categoryId: 'website', defaultTemplateId: 'website-starter', templateIds: ['website-starter'] }, - { id: 'hytopia', name: 'Hytopia', categoryId: 'game', defaultTemplateId: 'hytopia-game-starter', templateIds: ['hytopia-game-starter'] }, - { id: 'monogame', name: 'MonoGame', categoryId: 'game', defaultTemplateId: 'generic-empty', templateIds: ['generic-empty'] }, - { id: 'nodejs', name: 'Node.js', categoryId: 'tool', defaultTemplateId: 'node-typescript-tool', templateIds: ['node-typescript-tool', 'generic-empty'] }, - { id: 'generic', name: 'Generic', categoryId: 'other', defaultTemplateId: 'generic-empty', templateIds: ['generic-empty'] } - ]; - this.templates = [ - { id: 'website-starter', name: 'Website Starter', description: 'Simple website scaffold', categoryId: 'website', frameworkId: 'web-generic', defaultRepositoryType: 'website' }, - { id: 'hytopia-game-starter', name: 'Hytopia Starter', description: 'Hytopia game scaffold', categoryId: 'game', frameworkId: 'hytopia', defaultRepositoryType: 'hytopia-game' }, - { id: 'node-typescript-tool', name: 'Node TypeScript', description: 'Node.js + TypeScript starter', categoryId: 'tool', frameworkId: 'nodejs', defaultRepositoryType: 'tool-project' }, - { id: 'generic-empty', name: 'Empty Project', description: 'Minimal scaffold', categoryId: 'other', frameworkId: 'generic', defaultRepositoryType: 'generic' } - ]; - } - - getCategoryById(categoryId) { - const id = String(categoryId || '').trim(); - return this.categories.find((category) => category.id === id) || null; - } - - getFrameworkById(frameworkId) { - const id = String(frameworkId || '').trim(); - return this.frameworks.find((framework) => framework.id === id) || null; - } - - getTemplateById(templateId) { - const id = String(templateId || '').trim(); - return this.templates.find((template) => template.id === id) || null; - } - - getFrameworksForCategory(categoryId) { - const id = String(categoryId || '').trim(); - if (!id) return []; - const byCategory = this.frameworks.filter((framework) => framework.categoryId === id); - if (byCategory.length) return byCategory; - const category = this.getCategoryById(id); - if (!category) return []; - return this.frameworks.filter((framework) => (category.frameworkIds || []).includes(framework.id)); - } - - getTemplatesForFramework(frameworkId) { - const id = String(frameworkId || '').trim(); - if (!id) return []; - const framework = this.getFrameworkById(id); - if (!framework) return []; - const ids = Array.isArray(framework.templateIds) ? framework.templateIds : []; - const byFrameworkId = this.templates.filter((template) => template.frameworkId === id); - if (ids.length) { - const idSet = new Set(ids); - const ordered = []; - for (const templateId of ids) { - const row = this.getTemplateById(templateId); - if (row) ordered.push(row); - } - for (const row of byFrameworkId) { - if (!idSet.has(row.id)) ordered.push(row); - } - return ordered; - } - return byFrameworkId; - } - - getTemplatesForCategory(categoryId) { - const id = String(categoryId || '').trim(); - if (!id) return []; - return this.templates.filter((template) => template.categoryId === id); - } - - getSelectedCategory() { - return this.getCategoryById(this.data.category); - } - - getSelectedFramework() { - return this.getFrameworkById(this.data.framework); - } - - getSelectedTemplate() { - return this.getTemplateById(this.data.template); - } - - getCurrentRepositoryTypeHint() { - const sessionId = this.orchestrator?.focusedTerminalInfo?.sessionId || this.orchestrator?.lastInteractedSessionId || ''; - const session = sessionId && this.orchestrator?.sessions?.get ? this.orchestrator.sessions.get(sessionId) : null; - if (session?.repositoryType) return String(session.repositoryType).trim(); - - const workspace = this.orchestrator?.currentWorkspace || null; - if (!workspace) return ''; - - if (workspace.workspaceType === 'mixed-repo') { - const terminals = Array.isArray(workspace.terminals) ? workspace.terminals : workspace.terminals?.pairs; - const first = Array.isArray(terminals) && terminals.length ? terminals[0] : null; - return String(first?.repository?.type || '').trim(); - } - - return String(workspace.type || '').trim(); - } - - applyContextSuggestion() { - const repoType = this.getCurrentRepositoryTypeHint(); - if (!repoType) return; - - const template = this.templates.find((row) => String(row?.defaultRepositoryType || '').trim().toLowerCase() === repoType.toLowerCase()); - if (!template) return; - - const framework = this.getFrameworkById(template.frameworkId); - const categoryId = template.categoryId || framework?.categoryId || ''; - if (!categoryId) return; - - this.contextSuggestion = { - repositoryType: repoType, - categoryId, - frameworkId: framework?.id || '', - templateId: template.id - }; - - if (!this.data.category) this.data.category = categoryId; - if (!this.data.framework && framework?.id) this.data.framework = framework.id; - if (!this.data.template) this.data.template = template.id; - } - - ensureValidSelection() { - if (!this.data.category && this.categories.length) { - this.data.category = this.categories[0].id; - } - - const category = this.getSelectedCategory(); - if (!category) return; - - const frameworks = this.getFrameworksForCategory(category.id); - if (!frameworks.length) { - this.data.framework = ''; - this.data.template = ''; - return; - } - - const selectedFramework = frameworks.find((framework) => framework.id === this.data.framework); - const framework = selectedFramework || frameworks[0]; - this.data.framework = framework.id; - - const templates = this.getTemplatesForFramework(framework.id); - if (!templates.length) { - this.data.template = ''; - return; + console.error('Failed to load categories:', error); + this.categories = [ + { id: 'website', path: '~/GitHub/websites', keywords: ['website'] }, + { id: 'game', path: '~/GitHub/games', keywords: ['game'] }, + { id: 'tool', path: '~/GitHub/tools', keywords: ['tool'] }, + { id: 'other', path: '~/GitHub/projects', keywords: [] } + ]; } - - const preferredTemplateId = this.data.template - || framework.defaultTemplateId - || category.defaultTemplateId - || templates[0].id; - const selectedTemplate = templates.find((template) => template.id === preferredTemplateId) || templates[0]; - this.data.template = selectedTemplate.id; - } - - setCategory(categoryId) { - const next = String(categoryId || '').trim(); - if (!next) return; - this.data.category = next; - this.ensureValidSelection(); - } - - setFramework(frameworkId) { - const next = String(frameworkId || '').trim(); - if (!next) return; - this.data.framework = next; - this.ensureValidSelection(); - } - - setTemplate(templateId) { - const next = String(templateId || '').trim(); - if (!next) return; - this.data.template = next; - this.ensureValidSelection(); } renderWizard() { // Remove existing wizard const existing = document.getElementById('greenfield-wizard'); - if (existing) this.closeWizard(); + if (existing) existing.remove(); // Create wizard modal const wizard = document.createElement('div'); wizard.id = 'greenfield-wizard'; wizard.className = 'modal greenfield-wizard-modal'; wizard.innerHTML = ` -