all-hands-cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.allhands/README.md +75 -0
- package/.allhands/agents/compounder.yaml +15 -0
- package/.allhands/agents/coordinator.yaml +17 -0
- package/.allhands/agents/documentor.yaml +15 -0
- package/.allhands/agents/e2e-test-planner.yaml +17 -0
- package/.allhands/agents/emergent.yaml +22 -0
- package/.allhands/agents/executor.yaml +14 -0
- package/.allhands/agents/ideation.yaml +11 -0
- package/.allhands/agents/initiative-steering.yaml +19 -0
- package/.allhands/agents/judge.yaml +13 -0
- package/.allhands/agents/planner.yaml +19 -0
- package/.allhands/agents/pr-reviewer.yaml +15 -0
- package/.allhands/docs.json +5 -0
- package/.allhands/docs.local.json +26 -0
- package/.allhands/flows/COMPOUNDING.md +203 -0
- package/.allhands/flows/COORDINATION.md +89 -0
- package/.allhands/flows/CORE.md +87 -0
- package/.allhands/flows/DOCUMENTATION.md +218 -0
- package/.allhands/flows/E2E_TEST_PLAN_BUILDING.md +140 -0
- package/.allhands/flows/EMERGENT_PLANNING.md +57 -0
- package/.allhands/flows/IDEATION_SCOPING.md +154 -0
- package/.allhands/flows/INITIATIVE_STEERING.md +110 -0
- package/.allhands/flows/JUDGE_REVIEWING.md +79 -0
- package/.allhands/flows/PROMPT_TASK_EXECUTION.md +68 -0
- package/.allhands/flows/PR_REVIEWING.md +43 -0
- package/.allhands/flows/SPEC_PLANNING.md +216 -0
- package/.allhands/flows/harness/WRITING_HARNESS_FLOWS.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_KNOWLEDGE.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_ORCHESTRATION.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_SKILLS.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_TOOLS.md +27 -0
- package/.allhands/flows/harness/WRITING_HARNESS_VALIDATION_TOOLING.md +27 -0
- package/.allhands/flows/shared/CODEBASE_UNDERSTANDING.md +72 -0
- package/.allhands/flows/shared/CREATE_HARNESS_SPEC.md +48 -0
- package/.allhands/flows/shared/CREATE_SPEC.md +41 -0
- package/.allhands/flows/shared/CREATE_VALIDATION_TOOLING_SPEC.md +70 -0
- package/.allhands/flows/shared/DOCUMENTATION_DISCOVERY.md +123 -0
- package/.allhands/flows/shared/DOCUMENTATION_WRITER.md +101 -0
- package/.allhands/flows/shared/EMERGENT_REFINEMENT_ANALYSIS.md +76 -0
- package/.allhands/flows/shared/EXTERNAL_TECH_GUIDANCE.md +97 -0
- package/.allhands/flows/shared/IDEATION_CODEBASE_GROUNDING.md +49 -0
- package/.allhands/flows/shared/PLAN_DEEPENING.md +152 -0
- package/.allhands/flows/shared/PROMPT_TASKS_CURATION.md +113 -0
- package/.allhands/flows/shared/PROMPT_VALIDATION_REVIEW.MD +99 -0
- package/.allhands/flows/shared/QUICK_PREMORTEM.md +70 -0
- package/.allhands/flows/shared/RESEARCH_GUIDANCE.md +38 -0
- package/.allhands/flows/shared/REVIEW_OPTIONS_BREAKDOWN.md +68 -0
- package/.allhands/flows/shared/SKILL_EXTRACTION.md +84 -0
- package/.allhands/flows/shared/SPEC_FLOW_ANALYSIS.md +119 -0
- package/.allhands/flows/shared/TDD_WORKFLOW.md +109 -0
- package/.allhands/flows/shared/UTILIZE_VALIDATION_TOOLING.md +84 -0
- package/.allhands/flows/shared/WRITING_HARNESS_FLOWS.md +11 -0
- package/.allhands/flows/shared/WRITING_HARNESS_MCP_TOOLS.md +84 -0
- package/.allhands/flows/shared/jury/ARCHITECTURE_REVIEW.md +91 -0
- package/.allhands/flows/shared/jury/BEST_PRACTICES_REVIEW.md +80 -0
- package/.allhands/flows/shared/jury/CLAIM_VERIFICATION_REVIEW.md +101 -0
- package/.allhands/flows/shared/jury/EXPECTATIONS_FIT_REVIEW.md +78 -0
- package/.allhands/flows/shared/jury/MAINTAINABILITY_REVIEW.md +110 -0
- package/.allhands/flows/shared/jury/PROMPTS_EXPECTATIONS_FIT.md +74 -0
- package/.allhands/flows/shared/jury/PROMPTS_FLOW_ANALYSIS.md +92 -0
- package/.allhands/flows/shared/jury/PROMPTS_YAGNI.md +78 -0
- package/.allhands/flows/shared/jury/PROMPT_PREMORTEM.md +125 -0
- package/.allhands/flows/shared/jury/SECURITY_REVIEW.md +86 -0
- package/.allhands/flows/shared/jury/YAGNI_REVIEW.md +82 -0
- package/.allhands/flows/wip/DEBUG_INVESTIGATION.md +162 -0
- package/.allhands/flows/wip/MEMORY_RECALL.md +62 -0
- package/.allhands/harness/ah +131 -0
- package/.allhands/harness/package-lock.json +5292 -0
- package/.allhands/harness/package.json +52 -0
- package/.allhands/harness/src/__tests__/e2e/commands.test.ts +307 -0
- package/.allhands/harness/src/__tests__/e2e/event-loop.test.ts +539 -0
- package/.allhands/harness/src/__tests__/e2e/hooks.test.ts +427 -0
- package/.allhands/harness/src/__tests__/e2e/new-initiative-routing.test.ts +137 -0
- package/.allhands/harness/src/__tests__/e2e/run-e2e.ts +109 -0
- package/.allhands/harness/src/__tests__/e2e/specs-type.test.ts +210 -0
- package/.allhands/harness/src/__tests__/e2e/validation-hooks.test.ts +669 -0
- package/.allhands/harness/src/__tests__/e2e/validation-path-consistency.test.ts +354 -0
- package/.allhands/harness/src/__tests__/e2e/validation.test.ts +528 -0
- package/.allhands/harness/src/__tests__/harness/assertions.ts +318 -0
- package/.allhands/harness/src/__tests__/harness/cli-runner.ts +359 -0
- package/.allhands/harness/src/__tests__/harness/fixture.ts +384 -0
- package/.allhands/harness/src/__tests__/harness/hook-runner.ts +411 -0
- package/.allhands/harness/src/__tests__/harness/index.ts +122 -0
- package/.allhands/harness/src/cli.ts +36 -0
- package/.allhands/harness/src/commands/complexity.ts +177 -0
- package/.allhands/harness/src/commands/context7.ts +202 -0
- package/.allhands/harness/src/commands/docs.ts +557 -0
- package/.allhands/harness/src/commands/hooks.ts +24 -0
- package/.allhands/harness/src/commands/index.ts +51 -0
- package/.allhands/harness/src/commands/knowledge.ts +382 -0
- package/.allhands/harness/src/commands/memories.ts +302 -0
- package/.allhands/harness/src/commands/notify.ts +61 -0
- package/.allhands/harness/src/commands/oracle.ts +158 -0
- package/.allhands/harness/src/commands/perplexity.ts +220 -0
- package/.allhands/harness/src/commands/planning.ts +245 -0
- package/.allhands/harness/src/commands/schema.ts +73 -0
- package/.allhands/harness/src/commands/skills.ts +128 -0
- package/.allhands/harness/src/commands/solutions.ts +353 -0
- package/.allhands/harness/src/commands/spawn.ts +158 -0
- package/.allhands/harness/src/commands/specs.ts +532 -0
- package/.allhands/harness/src/commands/tavily.ts +226 -0
- package/.allhands/harness/src/commands/tools.ts +579 -0
- package/.allhands/harness/src/commands/trace.ts +327 -0
- package/.allhands/harness/src/commands/tui.ts +960 -0
- package/.allhands/harness/src/commands/validate.ts +143 -0
- package/.allhands/harness/src/commands/validation-tools.ts +108 -0
- package/.allhands/harness/src/hooks/context.ts +1442 -0
- package/.allhands/harness/src/hooks/enforcement.ts +170 -0
- package/.allhands/harness/src/hooks/index.ts +54 -0
- package/.allhands/harness/src/hooks/lifecycle.ts +229 -0
- package/.allhands/harness/src/hooks/notification.ts +104 -0
- package/.allhands/harness/src/hooks/observability.ts +551 -0
- package/.allhands/harness/src/hooks/session.ts +88 -0
- package/.allhands/harness/src/hooks/shared.ts +815 -0
- package/.allhands/harness/src/hooks/transcript-parser.ts +208 -0
- package/.allhands/harness/src/hooks/validation.ts +617 -0
- package/.allhands/harness/src/lib/__tests__/ctags.test.ts +244 -0
- package/.allhands/harness/src/lib/__tests__/docs-validation.test.ts +344 -0
- package/.allhands/harness/src/lib/__tests__/mcp-runtime.test.ts +190 -0
- package/.allhands/harness/src/lib/__tests__/schema.test.ts +861 -0
- package/.allhands/harness/src/lib/base-command.ts +198 -0
- package/.allhands/harness/src/lib/cli-daemon.ts +343 -0
- package/.allhands/harness/src/lib/compaction.ts +313 -0
- package/.allhands/harness/src/lib/ctags.ts +497 -0
- package/.allhands/harness/src/lib/docs-validation.ts +907 -0
- package/.allhands/harness/src/lib/event-loop.ts +662 -0
- package/.allhands/harness/src/lib/flows.ts +155 -0
- package/.allhands/harness/src/lib/git.ts +276 -0
- package/.allhands/harness/src/lib/knowledge-worker.ts +72 -0
- package/.allhands/harness/src/lib/knowledge.ts +810 -0
- package/.allhands/harness/src/lib/llm.ts +255 -0
- package/.allhands/harness/src/lib/mcp-client.ts +432 -0
- package/.allhands/harness/src/lib/mcp-daemon.ts +486 -0
- package/.allhands/harness/src/lib/mcp-runtime.ts +418 -0
- package/.allhands/harness/src/lib/notification.ts +115 -0
- package/.allhands/harness/src/lib/opencode/index.ts +70 -0
- package/.allhands/harness/src/lib/opencode/profiles.ts +300 -0
- package/.allhands/harness/src/lib/opencode/prompts/codesearch.md +98 -0
- package/.allhands/harness/src/lib/opencode/prompts/knowledge-aggregator.md +67 -0
- package/.allhands/harness/src/lib/opencode/runner.ts +281 -0
- package/.allhands/harness/src/lib/oracle.ts +926 -0
- package/.allhands/harness/src/lib/planning-utils.ts +150 -0
- package/.allhands/harness/src/lib/planning.ts +605 -0
- package/.allhands/harness/src/lib/pr-review.ts +225 -0
- package/.allhands/harness/src/lib/prompts.ts +522 -0
- package/.allhands/harness/src/lib/schema.ts +418 -0
- package/.allhands/harness/src/lib/schemas/agent-profile.ts +141 -0
- package/.allhands/harness/src/lib/schemas/template-vars.ts +138 -0
- package/.allhands/harness/src/lib/session.ts +164 -0
- package/.allhands/harness/src/lib/specs.ts +348 -0
- package/.allhands/harness/src/lib/tldr.ts +829 -0
- package/.allhands/harness/src/lib/tmux.ts +1051 -0
- package/.allhands/harness/src/lib/trace-store.ts +714 -0
- package/.allhands/harness/src/mcp/__tests__/index.test.ts +46 -0
- package/.allhands/harness/src/mcp/_template.ts +47 -0
- package/.allhands/harness/src/mcp/filesystem.ts +33 -0
- package/.allhands/harness/src/mcp/index.ts +69 -0
- package/.allhands/harness/src/mcp/playwright.ts +34 -0
- package/.allhands/harness/src/mcp/xcodebuild.ts +29 -0
- package/.allhands/harness/src/schemas/docs.schema.json +44 -0
- package/.allhands/harness/src/schemas/settings.schema.json +214 -0
- package/.allhands/harness/src/tui/actions.ts +227 -0
- package/.allhands/harness/src/tui/file-viewer-modal.ts +270 -0
- package/.allhands/harness/src/tui/index.ts +1574 -0
- package/.allhands/harness/src/tui/modal.ts +232 -0
- package/.allhands/harness/src/tui/prompts-pane.ts +186 -0
- package/.allhands/harness/src/tui/status-pane.ts +434 -0
- package/.allhands/harness/tsconfig.json +22 -0
- package/.allhands/harness/vitest.config.ts +13 -0
- package/.allhands/pillars.md +33 -0
- package/.allhands/principles.md +88 -0
- package/.allhands/schemas/alignment.yaml +51 -0
- package/.allhands/schemas/documentation.yaml +10 -0
- package/.allhands/schemas/prompt.yaml +92 -0
- package/.allhands/schemas/skill.yaml +34 -0
- package/.allhands/schemas/solution.yaml +131 -0
- package/.allhands/schemas/spec.yaml +67 -0
- package/.allhands/schemas/validation-suite.yaml +49 -0
- package/.allhands/schemas/workflow.yaml +51 -0
- package/.allhands/settings.json +57 -0
- package/.allhands/skills/claude-code-patterns/SKILL.md +60 -0
- package/.allhands/skills/claude-code-patterns/docs/context-hygiene.md +19 -0
- package/.allhands/skills/harness-maintenance/SKILL.md +449 -0
- package/.allhands/skills/harness-maintenance/references/core-architecture.md +187 -0
- package/.allhands/skills/harness-maintenance/references/harness-skills.md +87 -0
- package/.allhands/skills/harness-maintenance/references/knowledge-compounding.md +78 -0
- package/.allhands/skills/harness-maintenance/references/tools-commands-mcp-hooks.md +115 -0
- package/.allhands/skills/harness-maintenance/references/validation-tooling.md +77 -0
- package/.allhands/skills/harness-maintenance/references/writing-flows.md +84 -0
- package/.allhands/validation/browser-automation.md +109 -0
- package/.allhands/validation/xcode-automation.md +195 -0
- package/.allhands/workflows/documentation.md +86 -0
- package/.allhands/workflows/investigation.md +81 -0
- package/.allhands/workflows/milestone.md +91 -0
- package/.allhands/workflows/optimization.md +85 -0
- package/.allhands/workflows/refactor.md +99 -0
- package/.allhands/workflows/triage.md +81 -0
- package/.claude/README.md +1 -0
- package/.claude/agents/explorer.md +10 -0
- package/.claude/agents/researcher.md +11 -0
- package/.claude/agents/task-runner.md +8 -0
- package/.claude/settings.json +231 -0
- package/.env.ai.example +7 -0
- package/.github/workflows/npm-publish.yml +69 -0
- package/.internal.json +45 -0
- package/.tldr/config.json +11 -0
- package/.tldrignore +90 -0
- package/CLAUDE.md +6 -0
- package/README.md +98 -0
- package/bin/sync-cli.js +7552 -0
- package/concerns.md +7 -0
- package/docs/README.md +41 -0
- package/docs/agents/README.md +24 -0
- package/docs/agents/agent-configuration-system.md +86 -0
- package/docs/agents/execution-agents.md +50 -0
- package/docs/agents/knowledge-agents.md +61 -0
- package/docs/agents/orchestration-agent.md +57 -0
- package/docs/agents/planning-agents.md +84 -0
- package/docs/agents/quality-review-agents.md +67 -0
- package/docs/agents/workflow-agent-orchestration.md +69 -0
- package/docs/flows/README.md +44 -0
- package/docs/flows/compounding.md +126 -0
- package/docs/flows/coordination.md +72 -0
- package/docs/flows/core-harness-integration.md +63 -0
- package/docs/flows/documentation-orchestration.md +98 -0
- package/docs/flows/e2e-test-plan-building.md +83 -0
- package/docs/flows/emergent-refinement.md +104 -0
- package/docs/flows/flow-authoring-and-mcp-tools.md +89 -0
- package/docs/flows/judge-reviewing.md +112 -0
- package/docs/flows/plan-deepening-and-research.md +107 -0
- package/docs/flows/plan-review-jury.md +114 -0
- package/docs/flows/pr-reviewing.md +54 -0
- package/docs/flows/prompt-task-execution.md +119 -0
- package/docs/flows/spec-planning.md +162 -0
- package/docs/flows/type-specific-scoping-flows.md +49 -0
- package/docs/flows/validation-and-skills-integration.md +145 -0
- package/docs/flows/wip/wip-flows.md +102 -0
- package/docs/harness/README.md +23 -0
- package/docs/harness/agent-profiles.md +84 -0
- package/docs/harness/cli/README.md +24 -0
- package/docs/harness/cli/cli-entry-and-command-discovery.md +91 -0
- package/docs/harness/cli/docs-command.md +87 -0
- package/docs/harness/cli/knowledge-command.md +91 -0
- package/docs/harness/cli/minor-cli-commands.md +65 -0
- package/docs/harness/cli/oracle-command.md +113 -0
- package/docs/harness/cli/planning-command.md +95 -0
- package/docs/harness/cli/schema-and-validation-commands.md +154 -0
- package/docs/harness/cli/search-commands.md +97 -0
- package/docs/harness/cli/spawn-command.md +136 -0
- package/docs/harness/cli/specs-command.md +102 -0
- package/docs/harness/cli/tools-command.md +122 -0
- package/docs/harness/cli/trace-command.md +122 -0
- package/docs/harness/cli-daemon.md +92 -0
- package/docs/harness/event-loop.md +184 -0
- package/docs/harness/hooks/README.md +15 -0
- package/docs/harness/hooks/context-hooks.md +96 -0
- package/docs/harness/hooks/lifecycle-and-observability-hooks.md +135 -0
- package/docs/harness/hooks/validation-hooks.md +97 -0
- package/docs/harness/test-harness.md +149 -0
- package/docs/harness/tui.md +176 -0
- package/docs/memories.md +20 -0
- package/docs/solutions/agentic-issues/premature-agent-deletion-tui-action-dependency-20260130.md +49 -0
- package/docs/solutions/agentic-issues/ref-anchor-scope-mismatch-skill-references-20260131.md +55 -0
- package/docs/solutions/agentic-issues/tautological-tests-routing-20260131.md +52 -0
- package/docs/solutions/integration_issue/blocktool-output-format-mismatch-hook-runner-20260130.md +52 -0
- package/docs/solutions/integration_issue/dual-validation-path-divergence-schema-20260130.md +66 -0
- package/docs/solutions/security-issues/unsanitized-domain-path-join-20260131.md +52 -0
- package/docs/solutions/test-failures/event-loop-mock-ordering-checkAgentWindows-20260130.md +63 -0
- package/docs/sync-cli/README.md +19 -0
- package/docs/sync-cli/cli-entrypoint-and-commands.md +39 -0
- package/docs/sync-cli/commands/README.md +11 -0
- package/docs/sync-cli/commands/pull-manifest-command.md +36 -0
- package/docs/sync-cli/commands/push-command.md +84 -0
- package/docs/sync-cli/commands/sync-command.md +71 -0
- package/docs/sync-cli/systems/README.md +14 -0
- package/docs/sync-cli/systems/git-and-github-integration.md +49 -0
- package/docs/sync-cli/systems/interactive-ui.md +43 -0
- package/docs/sync-cli/systems/manifest-and-distribution.md +51 -0
- package/docs/sync-cli/systems/path-resolution.md +42 -0
- package/package.json +46 -0
- package/scripts/install-shim.sh +40 -0
- package/scripts/pre-pack.sh +25 -0
- package/specs/harness-maintenance-skill.spec.md +138 -0
- package/specs/roadmap/git-spec-lifecycle-management.spec.md +113 -0
- package/specs/sync-init-flag.spec.md +117 -0
- package/specs/unified-workflow-orchestration.spec.md +250 -0
- package/specs/validation-tooling-practice.spec.md +98 -0
- package/specs/workflow-domain-configuration.spec.md +265 -0
- package/src/commands/pull-manifest.ts +31 -0
- package/src/commands/push.ts +344 -0
- package/src/commands/sync.ts +289 -0
- package/src/lib/constants.ts +10 -0
- package/src/lib/dotfiles.ts +36 -0
- package/src/lib/fs-utils.ts +18 -0
- package/src/lib/gh.ts +40 -0
- package/src/lib/git.ts +63 -0
- package/src/lib/gitignore.ts +167 -0
- package/src/lib/manifest.ts +121 -0
- package/src/lib/marker-sync.ts +39 -0
- package/src/lib/paths.ts +38 -0
- package/src/lib/target-lines.ts +66 -0
- package/src/lib/ui.ts +78 -0
- package/src/sync-cli.ts +120 -0
- package/target-lines.json +23 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,1574 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TUI - Terminal User Interface for All Hands
|
|
3
|
+
*
|
|
4
|
+
* Three-pane layout:
|
|
5
|
+
* - Actions Pane (left): Agent spawners, toggles, quit/refresh
|
|
6
|
+
* - Prompt List Pane (center): Prompts by status
|
|
7
|
+
* - Status Pane (right): Active agents grid
|
|
8
|
+
*
|
|
9
|
+
* Navigation:
|
|
10
|
+
* - Tab/Shift-Tab: Cycle panes
|
|
11
|
+
* - j/k: Navigate within pane
|
|
12
|
+
* - u/d: Page up/down
|
|
13
|
+
* - Space: Toggle/select
|
|
14
|
+
* - Esc: Close modals
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import blessed from 'blessed';
|
|
18
|
+
import { join } from 'path';
|
|
19
|
+
import { loadProjectSettings } from '../hooks/shared.js';
|
|
20
|
+
import { CLIDaemon } from '../lib/cli-daemon.js';
|
|
21
|
+
import { validateDocsAsync } from '../lib/docs-validation.js';
|
|
22
|
+
import { EventLoop } from '../lib/event-loop.js';
|
|
23
|
+
import { flowsToModalItems, loadAllFlows } from '../lib/flows.js';
|
|
24
|
+
import { KnowledgeService, reindexAllInWorker, reindexFromChangesInWorker } from '../lib/knowledge.js';
|
|
25
|
+
import { loadAllProfiles } from '../lib/opencode/index.js';
|
|
26
|
+
import { planningDirExists, readStatus, sanitizeBranchForDir } from '../lib/planning.js';
|
|
27
|
+
import { loadAllPrompts, type PromptFile } from '../lib/prompts.js';
|
|
28
|
+
import { clearTuiSession, getHubWindowId, getSpawnedWindows } from '../lib/session.js';
|
|
29
|
+
import { getSpecForBranch, getWorkflowDomain, loadAllSpecs, specsToModalItems, type SpecFile } from '../lib/specs.js';
|
|
30
|
+
import { buildSemanticIndexAsync, ensureTldrDaemon, hasSemanticIndex, isTldrInstalled, needsSemanticRebuild, warmCallGraph } from '../lib/tldr.js';
|
|
31
|
+
import { getCurrentSession, killWindow, listWindows, spawnCustomFlow } from '../lib/tmux.js';
|
|
32
|
+
import { clearLogs, logTuiError, logTuiLifecycle } from '../lib/trace-store.js';
|
|
33
|
+
import { ActionItem, createActionsPane, ToggleState } from './actions.js';
|
|
34
|
+
import { createFileViewer, FileViewer, getPlanningFilePath, getSpecFilePath } from './file-viewer-modal.js';
|
|
35
|
+
import { createModal, Modal } from './modal.js';
|
|
36
|
+
import { createPromptsPane, PromptItem } from './prompts-pane.js';
|
|
37
|
+
import { AgentInfo, createStatusPane, getSelectableItems } from './status-pane.js';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Shared workflow domain items for initiative and steering modals.
|
|
41
|
+
* Single source of truth for the 6 domain {id, label} pairs.
|
|
42
|
+
*/
|
|
43
|
+
export const WORKFLOW_DOMAIN_ITEMS: ReadonlyArray<{ id: string; label: string }> = [
|
|
44
|
+
{ id: 'milestone', label: 'Milestone — Feature development with deep ideation' },
|
|
45
|
+
{ id: 'investigation', label: 'Investigation — Debug / diagnose issues' },
|
|
46
|
+
{ id: 'optimization', label: 'Optimization — Performance / efficiency work' },
|
|
47
|
+
{ id: 'refactor', label: 'Refactor — Cleanup / tech debt' },
|
|
48
|
+
{ id: 'documentation', label: 'Documentation — Coverage gaps' },
|
|
49
|
+
{ id: 'triage', label: 'Triage — External signal analysis' },
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
export type PaneId = 'actions' | 'prompts' | 'status';
|
|
53
|
+
|
|
54
|
+
export type PRActionState = 'create-pr' | 'awaiting-review' | 'rerun-pr-review';
|
|
55
|
+
|
|
56
|
+
export interface TUIOptions {
|
|
57
|
+
onAction: (action: string, data?: Record<string, unknown>) => void;
|
|
58
|
+
onExit: () => void;
|
|
59
|
+
onSpawnExecutor?: (prompt: PromptFile, branch: string, specId: string) => void;
|
|
60
|
+
onSpawnEmergentPlanning?: (branch: string, specId: string) => void;
|
|
61
|
+
cwd?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface TUIState {
|
|
65
|
+
loopEnabled: boolean;
|
|
66
|
+
parallelEnabled: boolean;
|
|
67
|
+
prompts: PromptItem[];
|
|
68
|
+
activeAgents: AgentInfo[];
|
|
69
|
+
spec?: string;
|
|
70
|
+
branch?: string;
|
|
71
|
+
baseBranch?: string;
|
|
72
|
+
prActionState: PRActionState;
|
|
73
|
+
customFlowCounter: number;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export class TUI {
|
|
77
|
+
private screen: blessed.Widgets.Screen;
|
|
78
|
+
private actionsPane: blessed.Widgets.BoxElement;
|
|
79
|
+
private promptsPane: blessed.Widgets.BoxElement;
|
|
80
|
+
private statusPane: blessed.Widgets.BoxElement;
|
|
81
|
+
|
|
82
|
+
private state: TUIState;
|
|
83
|
+
private options: TUIOptions;
|
|
84
|
+
|
|
85
|
+
// Navigation state
|
|
86
|
+
private focusedPane: PaneId = 'actions';
|
|
87
|
+
private paneOrder: PaneId[] = ['actions', 'prompts', 'status'];
|
|
88
|
+
private selectedIndex: Record<PaneId, number> = {
|
|
89
|
+
actions: 0,
|
|
90
|
+
prompts: 0,
|
|
91
|
+
status: 0,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
// Modals
|
|
95
|
+
private activeModal: Modal | null = null;
|
|
96
|
+
private activeFileViewer: FileViewer | null = null;
|
|
97
|
+
private logEntries: string[] = [];
|
|
98
|
+
|
|
99
|
+
// Action items (for selection tracking)
|
|
100
|
+
private actionItems: ActionItem[] = [];
|
|
101
|
+
|
|
102
|
+
// Event loop daemon
|
|
103
|
+
private eventLoop: EventLoop | null = null;
|
|
104
|
+
|
|
105
|
+
// CLI daemon for fast hook execution
|
|
106
|
+
private cliDaemon: CLIDaemon | null = null;
|
|
107
|
+
|
|
108
|
+
// Original output functions for restoration on destroy
|
|
109
|
+
private originalStdoutWrite: typeof process.stdout.write | null = null;
|
|
110
|
+
private originalStderrWrite: typeof process.stderr.write | null = null;
|
|
111
|
+
private originalConsoleLog: typeof console.log | null = null;
|
|
112
|
+
private originalConsoleError: typeof console.error | null = null;
|
|
113
|
+
|
|
114
|
+
constructor(options: TUIOptions) {
|
|
115
|
+
this.options = options;
|
|
116
|
+
this.state = {
|
|
117
|
+
loopEnabled: false,
|
|
118
|
+
parallelEnabled: false,
|
|
119
|
+
prompts: [],
|
|
120
|
+
activeAgents: [],
|
|
121
|
+
prActionState: 'create-pr',
|
|
122
|
+
customFlowCounter: 0,
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
// Suppress terminal capability errors (e.g., xterm-ghostty.Setulc) during screen creation
|
|
126
|
+
// These errors come from blessed parsing terminfo and can go to stdout/stderr/console
|
|
127
|
+
const originalStdoutWrite = process.stdout.write.bind(process.stdout);
|
|
128
|
+
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
|
129
|
+
const originalConsoleLog = console.log.bind(console);
|
|
130
|
+
const originalConsoleError = console.error.bind(console);
|
|
131
|
+
|
|
132
|
+
const isTerminfoNoise = (str: string): boolean => {
|
|
133
|
+
return (
|
|
134
|
+
str.includes('Setulc') ||
|
|
135
|
+
str.includes('Error on xterm') ||
|
|
136
|
+
str.includes('stack.push') ||
|
|
137
|
+
str.includes('out.push') ||
|
|
138
|
+
str.includes('stack.pop') ||
|
|
139
|
+
str.includes('stack = []') ||
|
|
140
|
+
str.includes('var v,') ||
|
|
141
|
+
str.includes('return out.join') ||
|
|
142
|
+
/^"\s*\\u001b\[/.test(str) ||
|
|
143
|
+
/^\s*out = \[/.test(str)
|
|
144
|
+
);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
148
|
+
(process.stdout as any).write = (chunk: string | Uint8Array, ...args: unknown[]): boolean => {
|
|
149
|
+
const str = typeof chunk === 'string' ? chunk : chunk.toString();
|
|
150
|
+
if (isTerminfoNoise(str)) return true;
|
|
151
|
+
return originalStdoutWrite(chunk, ...(args as [BufferEncoding?, ((err?: Error | null) => void)?]));
|
|
152
|
+
};
|
|
153
|
+
|
|
154
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
155
|
+
(process.stderr as any).write = (chunk: string | Uint8Array, ...args: unknown[]): boolean => {
|
|
156
|
+
const str = typeof chunk === 'string' ? chunk : chunk.toString();
|
|
157
|
+
if (isTerminfoNoise(str)) return true;
|
|
158
|
+
return originalStderrWrite(chunk, ...(args as [BufferEncoding?, ((err?: Error | null) => void)?]));
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
162
|
+
console.log = (...args: any[]): void => {
|
|
163
|
+
const str = args.map(a => String(a)).join(' ');
|
|
164
|
+
if (isTerminfoNoise(str)) return;
|
|
165
|
+
originalConsoleLog(...args);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
169
|
+
console.error = (...args: any[]): void => {
|
|
170
|
+
const str = args.map(a => String(a)).join(' ');
|
|
171
|
+
if (isTerminfoNoise(str)) return;
|
|
172
|
+
originalConsoleError(...args);
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// Create screen with terminal compatibility options
|
|
176
|
+
this.screen = blessed.screen({
|
|
177
|
+
smartCSR: true,
|
|
178
|
+
title: 'All Hands - Agentic Harness',
|
|
179
|
+
fullUnicode: true,
|
|
180
|
+
warnings: false, // Suppress terminal capability warnings
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// Store originals for restore on destroy
|
|
184
|
+
this.originalStdoutWrite = originalStdoutWrite;
|
|
185
|
+
this.originalStderrWrite = originalStderrWrite;
|
|
186
|
+
this.originalConsoleLog = originalConsoleLog;
|
|
187
|
+
this.originalConsoleError = originalConsoleError;
|
|
188
|
+
|
|
189
|
+
// Create header (element attaches to screen via parent option)
|
|
190
|
+
this.createHeader();
|
|
191
|
+
|
|
192
|
+
// Create panes
|
|
193
|
+
this.actionsPane = createActionsPane(this.screen, this.getToggleState());
|
|
194
|
+
this.promptsPane = createPromptsPane(this.screen, this.state.prompts);
|
|
195
|
+
this.statusPane = createStatusPane(
|
|
196
|
+
this.screen,
|
|
197
|
+
this.state.activeAgents,
|
|
198
|
+
undefined,
|
|
199
|
+
this.state.spec,
|
|
200
|
+
this.state.branch,
|
|
201
|
+
this.state.baseBranch,
|
|
202
|
+
this.logEntries,
|
|
203
|
+
undefined, // fileStates - will be set on render
|
|
204
|
+
undefined // options - will be set on render
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// Build action items list for navigation
|
|
208
|
+
this.buildActionItems();
|
|
209
|
+
|
|
210
|
+
// Setup navigation
|
|
211
|
+
this.setupKeyBindings();
|
|
212
|
+
|
|
213
|
+
// Initialize event loop daemon
|
|
214
|
+
if (options.cwd) {
|
|
215
|
+
this.eventLoop = new EventLoop(options.cwd, {
|
|
216
|
+
onPRReviewFeedback: (available: boolean) => {
|
|
217
|
+
if (available && this.state.prActionState === 'awaiting-review') {
|
|
218
|
+
this.state.prActionState = 'rerun-pr-review';
|
|
219
|
+
this.buildActionItems();
|
|
220
|
+
this.log('PR review feedback available - ready to review or rerun');
|
|
221
|
+
this.render();
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
onBranchChange: (newBranch, newSpec) => {
|
|
225
|
+
this.log(`Branch changed to: ${newBranch}`);
|
|
226
|
+
|
|
227
|
+
const updates: Partial<TUIState> = { branch: newBranch };
|
|
228
|
+
const newSpecId = newSpec?.id;
|
|
229
|
+
|
|
230
|
+
if (newSpecId !== this.state.spec) {
|
|
231
|
+
updates.spec = newSpecId;
|
|
232
|
+
|
|
233
|
+
if (this.options.cwd) {
|
|
234
|
+
const planningKey = sanitizeBranchForDir(newBranch);
|
|
235
|
+
if (planningDirExists(planningKey, this.options.cwd)) {
|
|
236
|
+
const prompts = loadAllPrompts(planningKey, this.options.cwd);
|
|
237
|
+
const status = readStatus(planningKey, this.options.cwd);
|
|
238
|
+
|
|
239
|
+
updates.prompts = prompts.map((p: { path: string; frontmatter: { number: number; title: string; status: string } }) => ({
|
|
240
|
+
number: p.frontmatter.number,
|
|
241
|
+
title: p.frontmatter.title,
|
|
242
|
+
status: p.frontmatter.status as 'pending' | 'in_progress' | 'done',
|
|
243
|
+
path: p.path,
|
|
244
|
+
}));
|
|
245
|
+
// Don't restore loopEnabled from status - always requires manual enable
|
|
246
|
+
this.state.parallelEnabled = status?.loop?.parallel ?? false;
|
|
247
|
+
} else {
|
|
248
|
+
this.state.prompts = [];
|
|
249
|
+
this.state.loopEnabled = false;
|
|
250
|
+
this.state.parallelEnabled = false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Sync toggle states to event loop
|
|
254
|
+
this.eventLoop?.setParallelEnabled(this.state.parallelEnabled);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (newSpec) {
|
|
258
|
+
this.log(`Spec: ${newSpec.id}`);
|
|
259
|
+
} else {
|
|
260
|
+
this.log('No spec for this branch');
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
this.updateState(updates);
|
|
265
|
+
},
|
|
266
|
+
onAgentsChange: (agents) => {
|
|
267
|
+
this.state.activeAgents = agents.map((name) => ({
|
|
268
|
+
name,
|
|
269
|
+
agentType: name,
|
|
270
|
+
isRunning: true,
|
|
271
|
+
}));
|
|
272
|
+
this.render();
|
|
273
|
+
},
|
|
274
|
+
onSpawnExecutor: (prompt) => {
|
|
275
|
+
this.log(`Loop: Spawning executor for prompt ${prompt.frontmatter.number}`);
|
|
276
|
+
if (this.state.branch && this.options.onSpawnExecutor) {
|
|
277
|
+
// Use spec if available, otherwise fall back to planning key
|
|
278
|
+
const specId = this.state.spec || sanitizeBranchForDir(this.state.branch);
|
|
279
|
+
this.options.onSpawnExecutor(prompt, this.state.branch, specId);
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
onSpawnEmergentPlanning: () => {
|
|
283
|
+
this.log('Loop: Spawning emergent planner');
|
|
284
|
+
if (this.state.branch && this.options.onSpawnEmergentPlanning) {
|
|
285
|
+
const specId = this.state.spec || sanitizeBranchForDir(this.state.branch);
|
|
286
|
+
this.options.onSpawnEmergentPlanning(this.state.branch, specId);
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
onLoopStatus: (message) => {
|
|
290
|
+
this.log(`Loop: ${message}`);
|
|
291
|
+
},
|
|
292
|
+
onPromptsChange: (prompts, snapshot) => {
|
|
293
|
+
// Update TUI state when prompts are added, removed, or status changes
|
|
294
|
+
const prevCount = this.state.prompts.length;
|
|
295
|
+
this.state.prompts = prompts.map((p) => ({
|
|
296
|
+
number: p.frontmatter.number,
|
|
297
|
+
title: p.frontmatter.title,
|
|
298
|
+
status: p.frontmatter.status as 'pending' | 'in_progress' | 'done',
|
|
299
|
+
path: p.path,
|
|
300
|
+
}));
|
|
301
|
+
|
|
302
|
+
// Log meaningful changes
|
|
303
|
+
if (snapshot.count !== prevCount) {
|
|
304
|
+
this.log(`Prompts: ${snapshot.count} (${snapshot.pending} pending, ${snapshot.inProgress} in progress, ${snapshot.done} done)`);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
this.buildActionItems();
|
|
308
|
+
this.render();
|
|
309
|
+
},
|
|
310
|
+
});
|
|
311
|
+
this.eventLoop.start();
|
|
312
|
+
|
|
313
|
+
// Start CLI daemon for fast hook execution (if enabled in settings)
|
|
314
|
+
const settings = loadProjectSettings();
|
|
315
|
+
const daemonEnabled = settings?.daemon?.enabled !== false; // default true
|
|
316
|
+
if (daemonEnabled) {
|
|
317
|
+
this.cliDaemon = new CLIDaemon(options.cwd);
|
|
318
|
+
this.cliDaemon.start().then(() => {
|
|
319
|
+
this.log(`CLI daemon ready (${this.cliDaemon?.getHandlerCount() ?? 0} handlers)`);
|
|
320
|
+
this.render();
|
|
321
|
+
}).catch((e) => {
|
|
322
|
+
this.log(`CLI daemon failed: ${e instanceof Error ? e.message : e}`);
|
|
323
|
+
});
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Start background indexing (non-blocking)
|
|
327
|
+
this.startBackgroundIndexing();
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
// Initial render
|
|
331
|
+
this.render();
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Start background indexing of knowledge bases and validation.
|
|
336
|
+
* Non-blocking - progress is logged to status pane.
|
|
337
|
+
*/
|
|
338
|
+
private async startBackgroundIndexing(): Promise<void> {
|
|
339
|
+
if (!this.options.cwd) return;
|
|
340
|
+
|
|
341
|
+
this.log('Starting background index...');
|
|
342
|
+
this.render();
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
// Ensure TLDR daemon is running first (required for dirty file tracking)
|
|
346
|
+
if (isTldrInstalled()) {
|
|
347
|
+
const daemonStarted = await ensureTldrDaemon(this.options.cwd);
|
|
348
|
+
if (daemonStarted) {
|
|
349
|
+
this.log('TLDR daemon ready');
|
|
350
|
+
} else {
|
|
351
|
+
this.log('TLDR daemon failed to start');
|
|
352
|
+
}
|
|
353
|
+
this.render();
|
|
354
|
+
|
|
355
|
+
const needsIndex = !hasSemanticIndex(this.options.cwd);
|
|
356
|
+
const needsRebuild = needsSemanticRebuild(this.options.cwd);
|
|
357
|
+
|
|
358
|
+
if (needsIndex || needsRebuild) {
|
|
359
|
+
this.log(needsIndex ? 'Building semantic index for first run...' : 'Rebuilding semantic index (branch changed)...');
|
|
360
|
+
this.render();
|
|
361
|
+
const result = await buildSemanticIndexAsync(this.options.cwd, (msg) => {
|
|
362
|
+
this.log(msg);
|
|
363
|
+
this.render();
|
|
364
|
+
});
|
|
365
|
+
if (result.success) {
|
|
366
|
+
const langInfo = result.languages.length > 0 ? ` (${result.languages.join(', ')})` : '';
|
|
367
|
+
const countInfo = result.filesIndexed > 0 ? `${result.filesIndexed} files` : '';
|
|
368
|
+
this.log(`Semantic index ready${countInfo ? `: ${countInfo}` : ''}${langInfo} ✓`);
|
|
369
|
+
} else {
|
|
370
|
+
this.log('Semantic index failed');
|
|
371
|
+
}
|
|
372
|
+
this.render();
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Always run warm to build/update call graph cache
|
|
376
|
+
this.log('Warming call graph cache...');
|
|
377
|
+
this.render();
|
|
378
|
+
const warmResult = await warmCallGraph(this.options.cwd, (msg) => {
|
|
379
|
+
this.log(msg);
|
|
380
|
+
this.render();
|
|
381
|
+
});
|
|
382
|
+
if (warmResult.success) {
|
|
383
|
+
this.log(`Call graph ready: ${warmResult.files} files, ${warmResult.edges} edges ✓`);
|
|
384
|
+
} else {
|
|
385
|
+
this.log('Call graph warm skipped or failed');
|
|
386
|
+
}
|
|
387
|
+
this.render();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Validate agent profiles first
|
|
391
|
+
this.log('Validating agent profiles...');
|
|
392
|
+
this.render();
|
|
393
|
+
const { profiles, errors: profileErrors } = loadAllProfiles();
|
|
394
|
+
if (profileErrors.length > 0) {
|
|
395
|
+
for (const err of profileErrors) {
|
|
396
|
+
for (const e of err.errors) {
|
|
397
|
+
this.log(`⚠ Agent ${err.name}: ${e}`);
|
|
398
|
+
}
|
|
399
|
+
for (const w of err.warnings) {
|
|
400
|
+
this.log(`⚠ Agent ${err.name}: ${w}`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
} else {
|
|
404
|
+
this.log(`${profiles.length} agent profiles valid ✓`);
|
|
405
|
+
}
|
|
406
|
+
this.render();
|
|
407
|
+
|
|
408
|
+
// GC hint: reclaim memory from TLDR child process buffers before knowledge indexing
|
|
409
|
+
if (global.gc) {
|
|
410
|
+
global.gc();
|
|
411
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
const cwd = this.options.cwd;
|
|
415
|
+
const service = new KnowledgeService(cwd, { quiet: true });
|
|
416
|
+
|
|
417
|
+
// Smart incremental indexing: check if indexes exist before deciding strategy
|
|
418
|
+
// indexExists and getChangesFromGit are lightweight (no model needed) - keep in-process
|
|
419
|
+
const roadmapExists = service.indexExists('roadmap');
|
|
420
|
+
const docsExists = service.indexExists('docs');
|
|
421
|
+
|
|
422
|
+
// Log indexing decision to trace for debugging
|
|
423
|
+
logTuiLifecycle('indexing.start', {
|
|
424
|
+
roadmapExists,
|
|
425
|
+
docsExists,
|
|
426
|
+
strategy: (!roadmapExists || !docsExists) ? 'full' : 'incremental',
|
|
427
|
+
}, cwd);
|
|
428
|
+
|
|
429
|
+
const workerProgress = (msg: string) => {
|
|
430
|
+
this.log(msg);
|
|
431
|
+
this.render();
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
if (!roadmapExists || !docsExists) {
|
|
435
|
+
// Cold start: full index required (via worker process)
|
|
436
|
+
if (!roadmapExists) {
|
|
437
|
+
this.log('Building roadmap index (first run)...');
|
|
438
|
+
logTuiLifecycle('indexing.full', { index: 'roadmap', reason: 'index_missing' }, cwd);
|
|
439
|
+
this.render();
|
|
440
|
+
await reindexAllInWorker(cwd, 'roadmap', workerProgress);
|
|
441
|
+
}
|
|
442
|
+
if (!docsExists) {
|
|
443
|
+
this.log('Building docs index (first run)...');
|
|
444
|
+
logTuiLifecycle('indexing.full', { index: 'docs', reason: 'index_missing' }, cwd);
|
|
445
|
+
this.render();
|
|
446
|
+
await reindexAllInWorker(cwd, 'docs', workerProgress);
|
|
447
|
+
}
|
|
448
|
+
} else {
|
|
449
|
+
// Warm start: incremental update from git changes
|
|
450
|
+
const roadmapChanges = service.getChangesFromGit('roadmap');
|
|
451
|
+
const docsChanges = service.getChangesFromGit('docs');
|
|
452
|
+
|
|
453
|
+
// Log change detection results
|
|
454
|
+
logTuiLifecycle('indexing.changes_detected', {
|
|
455
|
+
roadmapChanges: roadmapChanges.length,
|
|
456
|
+
docsChanges: docsChanges.length,
|
|
457
|
+
roadmapChangeFiles: roadmapChanges.slice(0, 10).map(c => c.path),
|
|
458
|
+
docsChangeFiles: docsChanges.slice(0, 10).map(c => c.path),
|
|
459
|
+
}, cwd);
|
|
460
|
+
|
|
461
|
+
if (roadmapChanges.length > 0) {
|
|
462
|
+
this.log(`Updating roadmap index (${roadmapChanges.length} changes)...`);
|
|
463
|
+
logTuiLifecycle('indexing.incremental', { index: 'roadmap', changeCount: roadmapChanges.length }, cwd);
|
|
464
|
+
this.render();
|
|
465
|
+
await reindexFromChangesInWorker(cwd, 'roadmap', roadmapChanges, workerProgress);
|
|
466
|
+
} else {
|
|
467
|
+
this.log('Roadmap index up to date ✓');
|
|
468
|
+
logTuiLifecycle('indexing.skip', { index: 'roadmap', reason: 'no_changes' }, cwd);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
if (docsChanges.length > 0) {
|
|
472
|
+
this.log(`Updating docs index (${docsChanges.length} changes)...`);
|
|
473
|
+
logTuiLifecycle('indexing.incremental', { index: 'docs', changeCount: docsChanges.length }, cwd);
|
|
474
|
+
this.render();
|
|
475
|
+
await reindexFromChangesInWorker(cwd, 'docs', docsChanges, workerProgress);
|
|
476
|
+
} else {
|
|
477
|
+
this.log('Docs index up to date ✓');
|
|
478
|
+
logTuiLifecycle('indexing.skip', { index: 'docs', reason: 'no_changes' }, cwd);
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
logTuiLifecycle('indexing.complete', {}, cwd);
|
|
483
|
+
|
|
484
|
+
// Run docs validation
|
|
485
|
+
this.log('Validating documentation...');
|
|
486
|
+
this.render();
|
|
487
|
+
const docsPath = join(cwd, 'docs');
|
|
488
|
+
const excludePaths = ["docs/memories.md", "docs/solutions"].map((p) => join(cwd, p));
|
|
489
|
+
const validation = await validateDocsAsync(docsPath, cwd, { excludePaths });
|
|
490
|
+
|
|
491
|
+
if (validation.frontmatter_error_count > 0) {
|
|
492
|
+
this.log(`⚠ ${validation.frontmatter_error_count} frontmatter errors`);
|
|
493
|
+
for (const err of validation.frontmatter_errors) {
|
|
494
|
+
this.log(` → ${err.doc_file}: ${err.reason}`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
if (validation.stale_count > 0) {
|
|
498
|
+
this.log(`⚠ ${validation.stale_count} stale references`);
|
|
499
|
+
for (const ref of validation.stale) {
|
|
500
|
+
this.log(` → ${ref.doc_file}: ${ref.reference}`);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (validation.invalid_count > 0) {
|
|
504
|
+
this.log(`⚠ ${validation.invalid_count} invalid references`);
|
|
505
|
+
for (const ref of validation.invalid) {
|
|
506
|
+
this.log(` → ${ref.doc_file}: ${ref.reference} (${ref.reason})`);
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
this.log('Index ready ✓');
|
|
511
|
+
this.render();
|
|
512
|
+
} catch (err) {
|
|
513
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
514
|
+
this.log(`Index error: ${message}`);
|
|
515
|
+
logTuiError('backgroundIndexing', err instanceof Error ? err : message, {
|
|
516
|
+
spec: this.state.spec,
|
|
517
|
+
branch: this.state.branch,
|
|
518
|
+
}, this.options.cwd);
|
|
519
|
+
this.render();
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
private createHeader(): blessed.Widgets.BoxElement {
|
|
524
|
+
return blessed.box({
|
|
525
|
+
parent: this.screen,
|
|
526
|
+
top: 0,
|
|
527
|
+
left: 0,
|
|
528
|
+
width: '100%',
|
|
529
|
+
height: 3,
|
|
530
|
+
content: '{center}{bold}{#a78bfa-fg}ALL HANDS{/#a78bfa-fg} {#e0e7ff-fg}AGENTIC HARNESS{/#e0e7ff-fg}{/bold}{/center}',
|
|
531
|
+
tags: true,
|
|
532
|
+
style: {
|
|
533
|
+
fg: '#e0e7ff',
|
|
534
|
+
border: {
|
|
535
|
+
fg: '#4A34C5',
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
border: {
|
|
539
|
+
type: 'line',
|
|
540
|
+
},
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
private getToggleState(): ToggleState {
|
|
545
|
+
return {
|
|
546
|
+
loopEnabled: this.state.loopEnabled,
|
|
547
|
+
parallelEnabled: this.state.parallelEnabled,
|
|
548
|
+
prActionState: this.state.prActionState,
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private buildActionItems(): void {
|
|
553
|
+
this.actionItems = [
|
|
554
|
+
// Agent spawners — all always visible
|
|
555
|
+
{ id: 'coordinator', label: 'Coordinator', key: '1', type: 'action' },
|
|
556
|
+
{ id: 'new-initiative', label: 'New Initiative', key: '2', type: 'action' },
|
|
557
|
+
{ id: 'planner', label: 'Planner', key: '3', type: 'action' },
|
|
558
|
+
{ id: 'review-jury', label: 'Review Jury', key: '4', type: 'action' },
|
|
559
|
+
{ id: 'e2e-test-planner', label: 'E2E Test Plan', key: '5', type: 'action' },
|
|
560
|
+
{ id: 'pr-action', label: this.getPRActionLabel(), key: '6', type: 'action' },
|
|
561
|
+
{ id: 'review-pr', label: 'Address PR Review', key: '7', type: 'action' },
|
|
562
|
+
{ id: 'compound', label: 'Compound', key: '8', type: 'action' },
|
|
563
|
+
{ id: 'mark-completed', label: 'Complete', key: '9', type: 'action' },
|
|
564
|
+
{ id: 'switch-spec', label: 'Switch Workspace', key: '0', type: 'action' },
|
|
565
|
+
{ id: 'custom-flow', label: 'Custom Flow', key: '-', type: 'action' },
|
|
566
|
+
{ id: 'initiative-steering', label: 'Steer Initiative', key: '=', type: 'action' },
|
|
567
|
+
{ id: 'separator-toggles', label: '─ Toggles ─', type: 'separator' },
|
|
568
|
+
{ id: 'toggle-loop', label: 'Loop', key: 'O', type: 'toggle', checked: this.state.loopEnabled },
|
|
569
|
+
{ id: 'toggle-parallel', label: 'Parallel', key: 'P', type: 'toggle', checked: this.state.parallelEnabled },
|
|
570
|
+
{ id: 'separator-controls', label: '─ Controls ─', type: 'separator' },
|
|
571
|
+
{ id: 'view-logs', label: 'View Logs', key: 'V', type: 'action' },
|
|
572
|
+
{ id: 'clear-logs', label: 'Clear Logs', key: 'C', type: 'action' },
|
|
573
|
+
{ id: 'refresh', label: 'Refresh', key: 'R', type: 'action' },
|
|
574
|
+
{ id: 'quit', label: 'Quit', key: 'Q', type: 'action' },
|
|
575
|
+
];
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
private getPRActionLabel(): string {
|
|
579
|
+
switch (this.state.prActionState) {
|
|
580
|
+
case 'create-pr': return 'Create PR';
|
|
581
|
+
case 'awaiting-review': return 'Awaiting Review...';
|
|
582
|
+
case 'rerun-pr-review': return 'Rerun PR Review';
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
private getSelectableActionItems(): ActionItem[] {
|
|
587
|
+
return this.actionItems.filter(item => item.type !== 'separator');
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
private setupKeyBindings(): void {
|
|
591
|
+
// Quit on Ctrl-C
|
|
592
|
+
this.screen.key(['C-c'], () => {
|
|
593
|
+
this.handleAction('quit');
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
// Tab/Shift-Tab for pane cycling
|
|
597
|
+
this.screen.key(['tab'], () => {
|
|
598
|
+
this.cyclePane(1);
|
|
599
|
+
});
|
|
600
|
+
this.screen.key(['S-tab'], () => {
|
|
601
|
+
this.cyclePane(-1);
|
|
602
|
+
});
|
|
603
|
+
|
|
604
|
+
// Vim navigation within panes
|
|
605
|
+
this.screen.key(['j'], () => {
|
|
606
|
+
if (!this.activeModal && !this.activeFileViewer) {
|
|
607
|
+
this.navigatePane(1);
|
|
608
|
+
}
|
|
609
|
+
});
|
|
610
|
+
this.screen.key(['k'], () => {
|
|
611
|
+
if (!this.activeModal && !this.activeFileViewer) {
|
|
612
|
+
this.navigatePane(-1);
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
this.screen.key(['u'], () => {
|
|
616
|
+
if (!this.activeModal && !this.activeFileViewer) {
|
|
617
|
+
this.navigatePane(-10); // Page up
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
this.screen.key(['d'], () => {
|
|
621
|
+
if (!this.activeModal && !this.activeFileViewer) {
|
|
622
|
+
this.navigatePane(10); // Page down
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
// Space to select/toggle
|
|
627
|
+
this.screen.key(['space'], () => {
|
|
628
|
+
if (!this.activeModal && !this.activeFileViewer) {
|
|
629
|
+
this.selectCurrentItem();
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
// Enter to activate
|
|
634
|
+
this.screen.key(['enter'], () => {
|
|
635
|
+
if (!this.activeModal && !this.activeFileViewer) {
|
|
636
|
+
this.selectCurrentItem();
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
// Escape to close modals
|
|
641
|
+
this.screen.key(['escape'], () => {
|
|
642
|
+
if (this.activeFileViewer) {
|
|
643
|
+
this.closeFileViewer();
|
|
644
|
+
} else if (this.activeModal) {
|
|
645
|
+
this.closeModal();
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
|
|
649
|
+
// Hotkeys for actions (work globally, not just in actions pane)
|
|
650
|
+
// Uses the key property from action items for consistent mapping
|
|
651
|
+
const hotkeys = ['1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '-', '='];
|
|
652
|
+
hotkeys.forEach((key) => {
|
|
653
|
+
this.screen.key([key], () => {
|
|
654
|
+
if (!this.activeModal && !this.activeFileViewer) {
|
|
655
|
+
// Find the action item with this key
|
|
656
|
+
const matchingItem = this.actionItems.find(
|
|
657
|
+
(item) => item.key === key && item.type === 'action'
|
|
658
|
+
);
|
|
659
|
+
if (matchingItem) {
|
|
660
|
+
this.handleAction(matchingItem.id);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// Toggle hotkeys (O for lOop, P for Parallel)
|
|
667
|
+
this.screen.key(['o'], () => {
|
|
668
|
+
if (!this.activeModal) {
|
|
669
|
+
this.handleAction('toggle-loop');
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
this.screen.key(['p'], () => {
|
|
673
|
+
if (!this.activeModal) {
|
|
674
|
+
this.handleAction('toggle-parallel');
|
|
675
|
+
}
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
// Q for quit, R for refresh, L for view logs, C for clear logs
|
|
679
|
+
this.screen.key(['q'], () => {
|
|
680
|
+
if (!this.activeModal) {
|
|
681
|
+
this.handleAction('quit');
|
|
682
|
+
}
|
|
683
|
+
});
|
|
684
|
+
this.screen.key(['r'], () => {
|
|
685
|
+
if (!this.activeModal) {
|
|
686
|
+
this.handleAction('refresh');
|
|
687
|
+
}
|
|
688
|
+
});
|
|
689
|
+
this.screen.key(['v'], () => {
|
|
690
|
+
if (!this.activeModal) {
|
|
691
|
+
this.handleAction('view-logs');
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
this.screen.key(['c'], () => {
|
|
695
|
+
if (!this.activeModal) {
|
|
696
|
+
this.handleAction('clear-logs');
|
|
697
|
+
}
|
|
698
|
+
});
|
|
699
|
+
|
|
700
|
+
// Delete agent when 'x' is pressed and status pane is focused with agent selected
|
|
701
|
+
this.screen.key(['x'], () => {
|
|
702
|
+
if (!this.activeModal && this.focusedPane === 'status') {
|
|
703
|
+
this.deleteSelectedAgent();
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
private cyclePane(direction: number): void {
|
|
710
|
+
const currentIndex = this.paneOrder.indexOf(this.focusedPane);
|
|
711
|
+
const newIndex = (currentIndex + direction + this.paneOrder.length) % this.paneOrder.length;
|
|
712
|
+
this.focusedPane = this.paneOrder[newIndex];
|
|
713
|
+
this.render();
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
private navigatePane(delta: number): void {
|
|
717
|
+
const maxIndex = this.getMaxIndexForPane(this.focusedPane);
|
|
718
|
+
if (maxIndex < 0) return;
|
|
719
|
+
|
|
720
|
+
const currentIndex = this.selectedIndex[this.focusedPane];
|
|
721
|
+
let newIndex = currentIndex + delta;
|
|
722
|
+
|
|
723
|
+
// Clamp to valid range
|
|
724
|
+
newIndex = Math.max(0, Math.min(maxIndex, newIndex));
|
|
725
|
+
this.selectedIndex[this.focusedPane] = newIndex;
|
|
726
|
+
|
|
727
|
+
this.render();
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
private getMaxIndexForPane(pane: PaneId): number {
|
|
731
|
+
switch (pane) {
|
|
732
|
+
case 'actions':
|
|
733
|
+
return this.getSelectableActionItems().length - 1;
|
|
734
|
+
case 'prompts':
|
|
735
|
+
return Math.max(0, this.state.prompts.length - 1);
|
|
736
|
+
case 'status':
|
|
737
|
+
// Status pane has docs + agents as selectable items
|
|
738
|
+
const fileStates = this.getFileStates();
|
|
739
|
+
const selectableItems = getSelectableItems(
|
|
740
|
+
this.state.spec,
|
|
741
|
+
fileStates,
|
|
742
|
+
this.state.activeAgents
|
|
743
|
+
);
|
|
744
|
+
return Math.max(0, selectableItems.length - 1);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
private selectCurrentItem(): void {
|
|
749
|
+
if (this.focusedPane === 'actions') {
|
|
750
|
+
const selectableItems = this.getSelectableActionItems();
|
|
751
|
+
const item = selectableItems[this.selectedIndex.actions];
|
|
752
|
+
if (item) {
|
|
753
|
+
this.handleAction(item.id);
|
|
754
|
+
}
|
|
755
|
+
} else if (this.focusedPane === 'prompts') {
|
|
756
|
+
const sortedPrompts = this.getSortedPrompts();
|
|
757
|
+
const prompt = sortedPrompts[this.selectedIndex.prompts];
|
|
758
|
+
if (prompt && prompt.path) {
|
|
759
|
+
// Open the prompt file in the file viewer
|
|
760
|
+
const title = `Prompt ${String(prompt.number).padStart(2, '0')}: ${prompt.title}`;
|
|
761
|
+
this.openFileViewer(title, prompt.path);
|
|
762
|
+
}
|
|
763
|
+
} else if (this.focusedPane === 'status') {
|
|
764
|
+
// Status pane: select docs or agents
|
|
765
|
+
const fileStates = this.getFileStates();
|
|
766
|
+
const selectableItems = getSelectableItems(
|
|
767
|
+
this.state.spec,
|
|
768
|
+
fileStates,
|
|
769
|
+
this.state.activeAgents
|
|
770
|
+
);
|
|
771
|
+
const item = selectableItems[this.selectedIndex.status];
|
|
772
|
+
if (item) {
|
|
773
|
+
switch (item.type) {
|
|
774
|
+
case 'spec':
|
|
775
|
+
if (this.state.spec && this.options.cwd) {
|
|
776
|
+
// Try branch first (status.yaml is in branch-based folder), then spec name
|
|
777
|
+
const specPath = getSpecFilePath(this.options.cwd, this.state.branch || this.state.spec);
|
|
778
|
+
if (specPath) {
|
|
779
|
+
this.openFileViewer(`Spec: ${this.state.spec}`, specPath);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
break;
|
|
783
|
+
case 'alignment':
|
|
784
|
+
if (this.state.branch && this.options.cwd) {
|
|
785
|
+
const alignPath = getPlanningFilePath(this.options.cwd, this.state.branch, 'alignment');
|
|
786
|
+
if (alignPath) {
|
|
787
|
+
this.openFileViewer('Alignment Document', alignPath);
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
break;
|
|
791
|
+
case 'e2e':
|
|
792
|
+
if (this.state.branch && this.options.cwd) {
|
|
793
|
+
const e2ePath = getPlanningFilePath(this.options.cwd, this.state.branch, 'e2e_test_plan');
|
|
794
|
+
if (e2ePath) {
|
|
795
|
+
this.openFileViewer('E2E Test Plan', e2ePath);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
break;
|
|
799
|
+
case 'agent':
|
|
800
|
+
// For agents, Enter could show details - for now just log
|
|
801
|
+
this.log(`Selected agent: ${item.agentName}`);
|
|
802
|
+
break;
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
}
|
|
807
|
+
|
|
808
|
+
/**
|
|
809
|
+
* Delete the currently selected agent (when status pane is focused)
|
|
810
|
+
*/
|
|
811
|
+
private deleteSelectedAgent(): void {
|
|
812
|
+
if (this.focusedPane !== 'status') return;
|
|
813
|
+
|
|
814
|
+
const fileStates = this.getFileStates();
|
|
815
|
+
const selectableItems = getSelectableItems(
|
|
816
|
+
this.state.spec,
|
|
817
|
+
fileStates,
|
|
818
|
+
this.state.activeAgents
|
|
819
|
+
);
|
|
820
|
+
const item = selectableItems[this.selectedIndex.status];
|
|
821
|
+
|
|
822
|
+
if (item?.type === 'agent' && item.agentName) {
|
|
823
|
+
const agentName = item.agentName;
|
|
824
|
+
// Find and kill the agent window
|
|
825
|
+
const currentSession = getCurrentSession();
|
|
826
|
+
if (currentSession) {
|
|
827
|
+
try {
|
|
828
|
+
killWindow(currentSession, agentName);
|
|
829
|
+
// Remove from active agents
|
|
830
|
+
this.state.activeAgents = this.state.activeAgents.filter(
|
|
831
|
+
(a) => a.name !== agentName
|
|
832
|
+
);
|
|
833
|
+
this.log(`Deleted agent: ${agentName}`);
|
|
834
|
+
// Adjust selection if needed
|
|
835
|
+
const newMax = this.getMaxIndexForPane('status');
|
|
836
|
+
if (this.selectedIndex.status > newMax) {
|
|
837
|
+
this.selectedIndex.status = Math.max(0, newMax);
|
|
838
|
+
}
|
|
839
|
+
this.render();
|
|
840
|
+
} catch (e) {
|
|
841
|
+
this.log(`Failed to delete agent: ${agentName}`);
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
/**
|
|
848
|
+
* Get prompts sorted the same way they appear in the prompts pane.
|
|
849
|
+
* Order: in_progress first, then pending, then done (each sorted by number).
|
|
850
|
+
*/
|
|
851
|
+
private getSortedPrompts(): PromptItem[] {
|
|
852
|
+
const inProgress = this.state.prompts
|
|
853
|
+
.filter((p) => p.status === 'in_progress')
|
|
854
|
+
.sort((a, b) => a.number - b.number);
|
|
855
|
+
|
|
856
|
+
const pending = this.state.prompts
|
|
857
|
+
.filter((p) => p.status === 'pending')
|
|
858
|
+
.sort((a, b) => a.number - b.number);
|
|
859
|
+
|
|
860
|
+
const done = this.state.prompts
|
|
861
|
+
.filter((p) => p.status === 'done')
|
|
862
|
+
.sort((a, b) => a.number - b.number);
|
|
863
|
+
|
|
864
|
+
return [...inProgress, ...pending, ...done];
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
private handleAction(actionId: string): void {
|
|
868
|
+
switch (actionId) {
|
|
869
|
+
case 'quit':
|
|
870
|
+
this.destroy(); // Kill spawned agents and cleanup first
|
|
871
|
+
this.options.onExit();
|
|
872
|
+
break;
|
|
873
|
+
case 'refresh':
|
|
874
|
+
this.render();
|
|
875
|
+
break;
|
|
876
|
+
case 'toggle-loop':
|
|
877
|
+
this.state.loopEnabled = !this.state.loopEnabled;
|
|
878
|
+
this.buildActionItems();
|
|
879
|
+
if (this.eventLoop) {
|
|
880
|
+
this.eventLoop.setLoopEnabled(this.state.loopEnabled);
|
|
881
|
+
}
|
|
882
|
+
this.options.onAction('toggle-loop', { enabled: this.state.loopEnabled });
|
|
883
|
+
this.render();
|
|
884
|
+
break;
|
|
885
|
+
case 'toggle-parallel':
|
|
886
|
+
this.state.parallelEnabled = !this.state.parallelEnabled;
|
|
887
|
+
this.buildActionItems();
|
|
888
|
+
if (this.eventLoop) {
|
|
889
|
+
this.eventLoop.setParallelEnabled(this.state.parallelEnabled);
|
|
890
|
+
// Force tick when enabling to spawn immediately
|
|
891
|
+
if (this.state.parallelEnabled) {
|
|
892
|
+
this.eventLoop.forceTick();
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
this.options.onAction('toggle-parallel', { enabled: this.state.parallelEnabled });
|
|
896
|
+
this.render();
|
|
897
|
+
break;
|
|
898
|
+
case 'view-logs':
|
|
899
|
+
this.openLogModal();
|
|
900
|
+
break;
|
|
901
|
+
case 'clear-logs':
|
|
902
|
+
this.clearAllLogs();
|
|
903
|
+
break;
|
|
904
|
+
case 'switch-spec':
|
|
905
|
+
this.openSpecModal();
|
|
906
|
+
break;
|
|
907
|
+
case 'custom-flow':
|
|
908
|
+
this.openCustomFlowModal();
|
|
909
|
+
break;
|
|
910
|
+
case 'pr-action':
|
|
911
|
+
if (this.state.prActionState === 'create-pr') {
|
|
912
|
+
this.options.onAction('create-pr');
|
|
913
|
+
} else if (this.state.prActionState === 'rerun-pr-review') {
|
|
914
|
+
this.options.onAction('rerun-pr-review');
|
|
915
|
+
}
|
|
916
|
+
break;
|
|
917
|
+
case 'review-pr':
|
|
918
|
+
this.options.onAction('review-pr');
|
|
919
|
+
break;
|
|
920
|
+
case 'new-initiative':
|
|
921
|
+
this.openNewInitiativeModal();
|
|
922
|
+
break;
|
|
923
|
+
case 'initiative-steering':
|
|
924
|
+
this.openSteeringDomainModal();
|
|
925
|
+
break;
|
|
926
|
+
default:
|
|
927
|
+
this.options.onAction(actionId);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Open a modal for selecting the spec type for a new initiative.
|
|
933
|
+
* Routes to the appropriate scoping flow based on selection.
|
|
934
|
+
*/
|
|
935
|
+
private openNewInitiativeModal(): void {
|
|
936
|
+
this.activeModal = createModal(this.screen, {
|
|
937
|
+
title: 'New Initiative — Select Type',
|
|
938
|
+
items: WORKFLOW_DOMAIN_ITEMS.map((t) => ({
|
|
939
|
+
id: t.id,
|
|
940
|
+
label: t.label,
|
|
941
|
+
type: 'item' as const,
|
|
942
|
+
})),
|
|
943
|
+
onSelect: (specType: string) => {
|
|
944
|
+
this.closeModal();
|
|
945
|
+
this.options.onAction('new-initiative', { specType });
|
|
946
|
+
},
|
|
947
|
+
onCancel: () => {
|
|
948
|
+
this.closeModal();
|
|
949
|
+
},
|
|
950
|
+
});
|
|
951
|
+
this.screen.render();
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
/**
|
|
955
|
+
* Open a modal for selecting the workflow domain when steering an initiative.
|
|
956
|
+
* Pre-selects the spec's initial_workflow_domain as the default.
|
|
957
|
+
*/
|
|
958
|
+
private openSteeringDomainModal(): void {
|
|
959
|
+
// Read spec's initial_workflow_domain for pre-selection
|
|
960
|
+
let defaultDomain = 'milestone';
|
|
961
|
+
const branch = this.state.branch;
|
|
962
|
+
if (branch) {
|
|
963
|
+
const spec = getSpecForBranch(branch, this.options.cwd);
|
|
964
|
+
if (spec) {
|
|
965
|
+
defaultDomain = getWorkflowDomain(spec.path);
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// Reorder so the default domain appears first (pre-selected)
|
|
970
|
+
const sorted = [
|
|
971
|
+
...WORKFLOW_DOMAIN_ITEMS.filter((d) => d.id === defaultDomain),
|
|
972
|
+
...WORKFLOW_DOMAIN_ITEMS.filter((d) => d.id !== defaultDomain),
|
|
973
|
+
];
|
|
974
|
+
|
|
975
|
+
this.activeModal = createModal(this.screen, {
|
|
976
|
+
title: `Steer Initiative — Select Domain (default: ${defaultDomain})`,
|
|
977
|
+
items: sorted.map((d) => ({
|
|
978
|
+
id: d.id,
|
|
979
|
+
label: d.label,
|
|
980
|
+
type: 'item' as const,
|
|
981
|
+
})),
|
|
982
|
+
onSelect: (domain: string) => {
|
|
983
|
+
this.closeModal();
|
|
984
|
+
this.options.onAction('initiative-steering', { domain });
|
|
985
|
+
},
|
|
986
|
+
onCancel: () => {
|
|
987
|
+
this.closeModal();
|
|
988
|
+
},
|
|
989
|
+
});
|
|
990
|
+
this.screen.render();
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
private openSpecModal(): void {
|
|
994
|
+
// Load specs dynamically from filesystem
|
|
995
|
+
const specGroups = loadAllSpecs(this.options.cwd);
|
|
996
|
+
const items = specsToModalItems(specGroups);
|
|
997
|
+
|
|
998
|
+
this.activeModal = createModal(this.screen, {
|
|
999
|
+
title: this.state.spec ? `Select Spec (current: ${this.state.spec})` : 'Select Spec',
|
|
1000
|
+
items,
|
|
1001
|
+
onSelect: (id: string) => {
|
|
1002
|
+
this.closeModal();
|
|
1003
|
+
this.options.onAction('switch-spec', { specId: id });
|
|
1004
|
+
},
|
|
1005
|
+
onCancel: () => {
|
|
1006
|
+
this.closeModal();
|
|
1007
|
+
},
|
|
1008
|
+
});
|
|
1009
|
+
this.screen.render();
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
private openLogModal(): void {
|
|
1013
|
+
// Reverse logs so newest entries appear at the top
|
|
1014
|
+
const reversedLogs = [...this.logEntries].reverse();
|
|
1015
|
+
this.activeModal = createModal(this.screen, {
|
|
1016
|
+
title: 'Activity Log',
|
|
1017
|
+
items: reversedLogs.map((entry, i) => ({
|
|
1018
|
+
id: `log-${i}`,
|
|
1019
|
+
label: entry,
|
|
1020
|
+
type: 'item' as const,
|
|
1021
|
+
})),
|
|
1022
|
+
onSelect: () => {}, // Log items not selectable
|
|
1023
|
+
onCancel: () => {
|
|
1024
|
+
this.closeModal();
|
|
1025
|
+
},
|
|
1026
|
+
scrollable: true,
|
|
1027
|
+
});
|
|
1028
|
+
this.screen.render();
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
private openCustomFlowModal(): void {
|
|
1032
|
+
// Load flows from filesystem
|
|
1033
|
+
const flowGroups = loadAllFlows();
|
|
1034
|
+
const items = flowsToModalItems(flowGroups);
|
|
1035
|
+
|
|
1036
|
+
this.activeModal = createModal(this.screen, {
|
|
1037
|
+
title: 'Select Flow',
|
|
1038
|
+
items,
|
|
1039
|
+
onSelect: (flowPath: string) => {
|
|
1040
|
+
this.closeModal();
|
|
1041
|
+
// flowPath is the absolute path to the selected flow file
|
|
1042
|
+
if (!flowPath.startsWith('header-')) {
|
|
1043
|
+
this.openCustomMessageInput(flowPath);
|
|
1044
|
+
}
|
|
1045
|
+
},
|
|
1046
|
+
onCancel: () => {
|
|
1047
|
+
this.closeModal();
|
|
1048
|
+
},
|
|
1049
|
+
scrollable: true,
|
|
1050
|
+
});
|
|
1051
|
+
this.screen.render();
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
private openCustomMessageInput(flowPath: string): void {
|
|
1055
|
+
// Create an input modal for the custom message
|
|
1056
|
+
const width = 60;
|
|
1057
|
+
const height = 12;
|
|
1058
|
+
|
|
1059
|
+
const box = blessed.box({
|
|
1060
|
+
parent: this.screen,
|
|
1061
|
+
top: 'center',
|
|
1062
|
+
left: 'center',
|
|
1063
|
+
width,
|
|
1064
|
+
height,
|
|
1065
|
+
border: {
|
|
1066
|
+
type: 'line',
|
|
1067
|
+
},
|
|
1068
|
+
label: ' Custom Message (optional) ',
|
|
1069
|
+
tags: true,
|
|
1070
|
+
style: {
|
|
1071
|
+
border: {
|
|
1072
|
+
fg: '#a78bfa',
|
|
1073
|
+
},
|
|
1074
|
+
},
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
// Add description text
|
|
1078
|
+
blessed.text({
|
|
1079
|
+
parent: box,
|
|
1080
|
+
top: 1,
|
|
1081
|
+
left: 1,
|
|
1082
|
+
content: '{#c7d2fe-fg}Enter a custom message (system prompt).\nLeave empty to skip. Press Enter to confirm.{/#c7d2fe-fg}',
|
|
1083
|
+
tags: true,
|
|
1084
|
+
});
|
|
1085
|
+
|
|
1086
|
+
// Create textarea for input
|
|
1087
|
+
const textarea = blessed.textarea({
|
|
1088
|
+
parent: box,
|
|
1089
|
+
top: 4,
|
|
1090
|
+
left: 1,
|
|
1091
|
+
width: width - 4,
|
|
1092
|
+
height: 4,
|
|
1093
|
+
border: {
|
|
1094
|
+
type: 'line',
|
|
1095
|
+
},
|
|
1096
|
+
style: {
|
|
1097
|
+
border: {
|
|
1098
|
+
fg: '#4A34C5',
|
|
1099
|
+
},
|
|
1100
|
+
focus: {
|
|
1101
|
+
border: {
|
|
1102
|
+
fg: '#a78bfa',
|
|
1103
|
+
},
|
|
1104
|
+
},
|
|
1105
|
+
},
|
|
1106
|
+
inputOnFocus: true,
|
|
1107
|
+
});
|
|
1108
|
+
|
|
1109
|
+
// Help text
|
|
1110
|
+
blessed.text({
|
|
1111
|
+
parent: box,
|
|
1112
|
+
bottom: 0,
|
|
1113
|
+
left: 1,
|
|
1114
|
+
content: '{#5c6370-fg}[Enter] Confirm [Esc] Cancel{/#5c6370-fg}',
|
|
1115
|
+
tags: true,
|
|
1116
|
+
});
|
|
1117
|
+
|
|
1118
|
+
// Store modal reference for cleanup (conform to Modal interface)
|
|
1119
|
+
const modalRef: Modal = {
|
|
1120
|
+
box,
|
|
1121
|
+
selectedIndex: 0,
|
|
1122
|
+
destroy: () => {
|
|
1123
|
+
box.destroy();
|
|
1124
|
+
},
|
|
1125
|
+
navigate: () => {}, // Not used for input modal
|
|
1126
|
+
select: () => {}, // Not used for input modal
|
|
1127
|
+
};
|
|
1128
|
+
this.activeModal = modalRef;
|
|
1129
|
+
|
|
1130
|
+
// Focus textarea
|
|
1131
|
+
textarea.focus();
|
|
1132
|
+
|
|
1133
|
+
// Handle Enter key - submit
|
|
1134
|
+
textarea.key(['enter'], () => {
|
|
1135
|
+
const customMessage = textarea.getValue().trim();
|
|
1136
|
+
textarea.cancel(); // Exit input mode before destroying
|
|
1137
|
+
modalRef.destroy();
|
|
1138
|
+
this.activeModal = null;
|
|
1139
|
+
this.screen.focusPop(); // Restore focus to screen
|
|
1140
|
+
this.spawnCustomFlowAgent(flowPath, customMessage);
|
|
1141
|
+
this.render();
|
|
1142
|
+
});
|
|
1143
|
+
|
|
1144
|
+
// Handle Escape key - cancel
|
|
1145
|
+
textarea.key(['escape'], () => {
|
|
1146
|
+
textarea.cancel(); // Exit input mode before destroying
|
|
1147
|
+
modalRef.destroy();
|
|
1148
|
+
this.activeModal = null;
|
|
1149
|
+
this.screen.focusPop(); // Restore focus to screen
|
|
1150
|
+
this.render();
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
this.screen.render();
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
private spawnCustomFlowAgent(flowPath: string, customMessage: string): void {
|
|
1157
|
+
// Increment counter and generate window name
|
|
1158
|
+
this.state.customFlowCounter++;
|
|
1159
|
+
const windowName = `custom-flow-${this.state.customFlowCounter}`;
|
|
1160
|
+
const branch = this.state.branch || 'main';
|
|
1161
|
+
|
|
1162
|
+
this.log(`Spawning custom flow: ${windowName}`);
|
|
1163
|
+
this.log(`Flow: ${flowPath.split('/').slice(-2).join('/')}`);
|
|
1164
|
+
|
|
1165
|
+
try {
|
|
1166
|
+
const result = spawnCustomFlow(
|
|
1167
|
+
{
|
|
1168
|
+
flowPath,
|
|
1169
|
+
customMessage,
|
|
1170
|
+
windowName,
|
|
1171
|
+
focusWindow: true,
|
|
1172
|
+
specName: this.state.spec,
|
|
1173
|
+
},
|
|
1174
|
+
branch,
|
|
1175
|
+
this.options.cwd
|
|
1176
|
+
);
|
|
1177
|
+
|
|
1178
|
+
this.log(`Spawned ${windowName} in ${result.sessionName}:${result.windowName}`);
|
|
1179
|
+
|
|
1180
|
+
// Update running agents display
|
|
1181
|
+
this.state.activeAgents = [
|
|
1182
|
+
...this.state.activeAgents,
|
|
1183
|
+
{
|
|
1184
|
+
name: windowName,
|
|
1185
|
+
agentType: 'custom-flow',
|
|
1186
|
+
isRunning: true,
|
|
1187
|
+
},
|
|
1188
|
+
];
|
|
1189
|
+
this.render();
|
|
1190
|
+
} catch (e) {
|
|
1191
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
1192
|
+
this.log(`Error spawning custom flow: ${message}`);
|
|
1193
|
+
logTuiError('spawnCustomFlow', e instanceof Error ? e : message, {
|
|
1194
|
+
flowPath,
|
|
1195
|
+
windowName,
|
|
1196
|
+
customMessage: customMessage || undefined,
|
|
1197
|
+
spec: this.state.spec,
|
|
1198
|
+
branch: this.state.branch,
|
|
1199
|
+
}, this.options.cwd);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
private closeModal(): void {
|
|
1204
|
+
if (this.activeModal) {
|
|
1205
|
+
this.activeModal.destroy();
|
|
1206
|
+
this.activeModal = null;
|
|
1207
|
+
this.render();
|
|
1208
|
+
}
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
/**
|
|
1212
|
+
* Show a confirmation dialog and wait for user response.
|
|
1213
|
+
* Returns true if user confirms, false if cancelled.
|
|
1214
|
+
* Optional detail text renders below the message in a dimmer style.
|
|
1215
|
+
*/
|
|
1216
|
+
public showConfirmation(title: string, message: string, detail?: string): Promise<boolean> {
|
|
1217
|
+
return new Promise((resolve) => {
|
|
1218
|
+
if (this.activeModal) {
|
|
1219
|
+
this.closeModal();
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
const width = 60;
|
|
1223
|
+
const messageLines = message.split('\n');
|
|
1224
|
+
// Detail adds a blank separator line plus its own lines
|
|
1225
|
+
const detailLines = detail ? detail.split('\n') : [];
|
|
1226
|
+
const totalContentLines = messageLines.length + (detail ? 1 + detailLines.length : 0);
|
|
1227
|
+
const height = Math.min(totalContentLines + 6, 20);
|
|
1228
|
+
|
|
1229
|
+
const box = blessed.box({
|
|
1230
|
+
parent: this.screen,
|
|
1231
|
+
top: 'center',
|
|
1232
|
+
left: 'center',
|
|
1233
|
+
width,
|
|
1234
|
+
height,
|
|
1235
|
+
border: {
|
|
1236
|
+
type: 'line',
|
|
1237
|
+
},
|
|
1238
|
+
label: ` ${title} `,
|
|
1239
|
+
tags: true,
|
|
1240
|
+
style: {
|
|
1241
|
+
border: {
|
|
1242
|
+
fg: '#a78bfa',
|
|
1243
|
+
},
|
|
1244
|
+
},
|
|
1245
|
+
});
|
|
1246
|
+
|
|
1247
|
+
// Add message text
|
|
1248
|
+
blessed.text({
|
|
1249
|
+
parent: box,
|
|
1250
|
+
top: 1,
|
|
1251
|
+
left: 2,
|
|
1252
|
+
right: 2,
|
|
1253
|
+
content: `{#c7d2fe-fg}${message}{/#c7d2fe-fg}`,
|
|
1254
|
+
tags: true,
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
// Add detail text below message if provided
|
|
1258
|
+
if (detail) {
|
|
1259
|
+
blessed.text({
|
|
1260
|
+
parent: box,
|
|
1261
|
+
top: 1 + messageLines.length + 1, // message offset + message lines + blank separator
|
|
1262
|
+
left: 2,
|
|
1263
|
+
right: 2,
|
|
1264
|
+
content: `{#5c6370-fg}${detail}{/#5c6370-fg}`,
|
|
1265
|
+
tags: true,
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
// Add button hints
|
|
1270
|
+
blessed.text({
|
|
1271
|
+
parent: box,
|
|
1272
|
+
bottom: 0,
|
|
1273
|
+
left: 1,
|
|
1274
|
+
content: '{#10b981-fg}[Enter]{/#10b981-fg} Proceed {#ef4444-fg}[Esc]{/#ef4444-fg} Cancel',
|
|
1275
|
+
tags: true,
|
|
1276
|
+
});
|
|
1277
|
+
|
|
1278
|
+
// Focus the box for key events
|
|
1279
|
+
box.focus();
|
|
1280
|
+
|
|
1281
|
+
// Store modal reference
|
|
1282
|
+
const modalRef: Modal = {
|
|
1283
|
+
box,
|
|
1284
|
+
selectedIndex: 0,
|
|
1285
|
+
destroy: () => box.destroy(),
|
|
1286
|
+
navigate: () => {},
|
|
1287
|
+
select: () => {},
|
|
1288
|
+
};
|
|
1289
|
+
this.activeModal = modalRef;
|
|
1290
|
+
|
|
1291
|
+
// Handle Enter - confirm
|
|
1292
|
+
box.key(['enter'], () => {
|
|
1293
|
+
modalRef.destroy();
|
|
1294
|
+
this.activeModal = null;
|
|
1295
|
+
this.screen.focusPop();
|
|
1296
|
+
this.render();
|
|
1297
|
+
resolve(true);
|
|
1298
|
+
});
|
|
1299
|
+
|
|
1300
|
+
// Handle Escape - cancel
|
|
1301
|
+
box.key(['escape'], () => {
|
|
1302
|
+
modalRef.destroy();
|
|
1303
|
+
this.activeModal = null;
|
|
1304
|
+
this.screen.focusPop();
|
|
1305
|
+
this.render();
|
|
1306
|
+
resolve(false);
|
|
1307
|
+
});
|
|
1308
|
+
|
|
1309
|
+
this.screen.render();
|
|
1310
|
+
});
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
public openFileViewer(title: string, filePath: string): void {
|
|
1314
|
+
if (this.activeFileViewer) {
|
|
1315
|
+
this.closeFileViewer();
|
|
1316
|
+
}
|
|
1317
|
+
if (this.activeModal) {
|
|
1318
|
+
this.closeModal();
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
this.activeFileViewer = createFileViewer(this.screen, {
|
|
1322
|
+
title,
|
|
1323
|
+
filePath,
|
|
1324
|
+
onClose: () => {
|
|
1325
|
+
this.closeFileViewer();
|
|
1326
|
+
},
|
|
1327
|
+
});
|
|
1328
|
+
|
|
1329
|
+
if (!this.activeFileViewer) {
|
|
1330
|
+
this.log(`File not found: ${filePath}`);
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
private closeFileViewer(): void {
|
|
1335
|
+
if (this.activeFileViewer) {
|
|
1336
|
+
this.activeFileViewer.destroy();
|
|
1337
|
+
this.activeFileViewer = null;
|
|
1338
|
+
this.render();
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
/**
|
|
1343
|
+
* Get planning file paths for current spec
|
|
1344
|
+
*/
|
|
1345
|
+
public getFileStates(): { spec: boolean; alignment: boolean; e2eTestPlan: boolean } {
|
|
1346
|
+
if (!this.state.branch || !this.options.cwd) {
|
|
1347
|
+
return { spec: false, alignment: false, e2eTestPlan: false };
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1350
|
+
return {
|
|
1351
|
+
spec: this.state.spec ? getSpecFilePath(this.options.cwd, this.state.spec) !== null : false,
|
|
1352
|
+
alignment: getPlanningFilePath(this.options.cwd, this.state.branch, 'alignment') !== null,
|
|
1353
|
+
e2eTestPlan: getPlanningFilePath(this.options.cwd, this.state.branch, 'e2e_test_plan') !== null,
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
public updateState(updates: Partial<TUIState>): void {
|
|
1358
|
+
this.state = { ...this.state, ...updates };
|
|
1359
|
+
|
|
1360
|
+
// Sync toggle states to event loop if they were updated
|
|
1361
|
+
if ('parallelEnabled' in updates && this.eventLoop) {
|
|
1362
|
+
this.eventLoop.setParallelEnabled(this.state.parallelEnabled);
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
this.buildActionItems();
|
|
1366
|
+
this.render();
|
|
1367
|
+
}
|
|
1368
|
+
|
|
1369
|
+
public getState(): TUIState {
|
|
1370
|
+
return { ...this.state };
|
|
1371
|
+
}
|
|
1372
|
+
|
|
1373
|
+
public log(message: string): void {
|
|
1374
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
1375
|
+
this.logEntries.push(`[${timestamp}] ${message}`);
|
|
1376
|
+
// Keep last 100 entries
|
|
1377
|
+
while (this.logEntries.length > 100) {
|
|
1378
|
+
this.logEntries.shift();
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
/**
|
|
1383
|
+
* Clear all logs: both trace store (SQLite + JSONL) and in-memory TUI logs
|
|
1384
|
+
*/
|
|
1385
|
+
private clearAllLogs(): void {
|
|
1386
|
+
// Clear trace store logs
|
|
1387
|
+
clearLogs(this.options.cwd);
|
|
1388
|
+
|
|
1389
|
+
// Clear in-memory TUI logs
|
|
1390
|
+
this.logEntries = [];
|
|
1391
|
+
|
|
1392
|
+
this.log('Logs cleared');
|
|
1393
|
+
this.render();
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
/**
|
|
1397
|
+
* Set PR URL for PR review feedback monitoring
|
|
1398
|
+
*/
|
|
1399
|
+
public setPRUrl(url: string | null): void {
|
|
1400
|
+
if (this.eventLoop) {
|
|
1401
|
+
this.eventLoop.setPRUrl(url);
|
|
1402
|
+
}
|
|
1403
|
+
if (url) {
|
|
1404
|
+
this.state.prActionState = 'awaiting-review';
|
|
1405
|
+
this.buildActionItems();
|
|
1406
|
+
this.render();
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
/**
|
|
1411
|
+
* Sync EventLoop's branch context after TUI-initiated branch changes.
|
|
1412
|
+
*
|
|
1413
|
+
* Call this after switch-spec, clear-spec, or mark-completed to prevent
|
|
1414
|
+
* the EventLoop from detecting a "stale" branch change and overwriting
|
|
1415
|
+
* the TUI's correct state with incorrect data from findSpecByBranch().
|
|
1416
|
+
*
|
|
1417
|
+
* @param branch - The new branch name
|
|
1418
|
+
* @param spec - The spec for this branch (or null if no spec)
|
|
1419
|
+
*/
|
|
1420
|
+
public syncBranchContext(branch: string, spec: SpecFile | null): void {
|
|
1421
|
+
if (this.eventLoop) {
|
|
1422
|
+
this.eventLoop.setBranchContext(branch, spec);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
private render(): void {
|
|
1427
|
+
// Update actions pane
|
|
1428
|
+
this.actionsPane.destroy();
|
|
1429
|
+
this.actionsPane = createActionsPane(
|
|
1430
|
+
this.screen,
|
|
1431
|
+
this.getToggleState(),
|
|
1432
|
+
this.focusedPane === 'actions' ? this.selectedIndex.actions : undefined
|
|
1433
|
+
);
|
|
1434
|
+
|
|
1435
|
+
// Update prompts pane
|
|
1436
|
+
this.promptsPane.destroy();
|
|
1437
|
+
this.promptsPane = createPromptsPane(
|
|
1438
|
+
this.screen,
|
|
1439
|
+
this.state.prompts,
|
|
1440
|
+
this.focusedPane === 'prompts' ? this.selectedIndex.prompts : undefined
|
|
1441
|
+
);
|
|
1442
|
+
|
|
1443
|
+
// Update status pane
|
|
1444
|
+
this.statusPane.destroy();
|
|
1445
|
+
const fileStates = this.getFileStates();
|
|
1446
|
+
this.statusPane = createStatusPane(
|
|
1447
|
+
this.screen,
|
|
1448
|
+
this.state.activeAgents,
|
|
1449
|
+
this.focusedPane === 'status' ? this.selectedIndex.status : undefined,
|
|
1450
|
+
this.state.spec,
|
|
1451
|
+
this.state.branch,
|
|
1452
|
+
this.state.baseBranch,
|
|
1453
|
+
this.logEntries,
|
|
1454
|
+
fileStates,
|
|
1455
|
+
{
|
|
1456
|
+
onViewSpec: () => {
|
|
1457
|
+
if (this.state.spec && this.options.cwd) {
|
|
1458
|
+
// Try branch first (status.yaml is in branch-based folder), then spec name
|
|
1459
|
+
const specPath = getSpecFilePath(this.options.cwd, this.state.branch || this.state.spec);
|
|
1460
|
+
if (specPath) {
|
|
1461
|
+
this.openFileViewer(`Spec: ${this.state.spec}`, specPath);
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
},
|
|
1465
|
+
onViewAlignment: () => {
|
|
1466
|
+
if (this.state.branch && this.options.cwd) {
|
|
1467
|
+
const alignPath = getPlanningFilePath(this.options.cwd, this.state.branch, 'alignment');
|
|
1468
|
+
if (alignPath) {
|
|
1469
|
+
this.openFileViewer('Alignment Document', alignPath);
|
|
1470
|
+
}
|
|
1471
|
+
}
|
|
1472
|
+
},
|
|
1473
|
+
onViewE2ETestPlan: () => {
|
|
1474
|
+
if (this.state.branch && this.options.cwd) {
|
|
1475
|
+
const e2ePath = getPlanningFilePath(this.options.cwd, this.state.branch, 'e2e_test_plan');
|
|
1476
|
+
if (e2ePath) {
|
|
1477
|
+
this.openFileViewer('E2E Test Plan', e2ePath);
|
|
1478
|
+
}
|
|
1479
|
+
}
|
|
1480
|
+
},
|
|
1481
|
+
}
|
|
1482
|
+
);
|
|
1483
|
+
|
|
1484
|
+
// Apply focus styling
|
|
1485
|
+
this.applyFocusStyles();
|
|
1486
|
+
|
|
1487
|
+
this.screen.render();
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
private applyFocusStyles(): void {
|
|
1491
|
+
// Highlight focused pane border
|
|
1492
|
+
const panes: Record<PaneId, blessed.Widgets.BoxElement> = {
|
|
1493
|
+
actions: this.actionsPane,
|
|
1494
|
+
prompts: this.promptsPane,
|
|
1495
|
+
status: this.statusPane,
|
|
1496
|
+
};
|
|
1497
|
+
|
|
1498
|
+
for (const [paneId, pane] of Object.entries(panes)) {
|
|
1499
|
+
const isFocused = paneId === this.focusedPane;
|
|
1500
|
+
if (pane.style && pane.style.border) {
|
|
1501
|
+
// Focused: bright purple, Unfocused: muted purple
|
|
1502
|
+
pane.style.border.fg = isFocused ? '#a78bfa' : '#4A34C5';
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
public destroy(): void {
|
|
1508
|
+
// Stop CLI daemon
|
|
1509
|
+
if (this.cliDaemon) {
|
|
1510
|
+
this.cliDaemon.stop();
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
// Stop event loop daemon
|
|
1514
|
+
if (this.eventLoop) {
|
|
1515
|
+
this.eventLoop.stop();
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
// Only kill windows that were spawned by this TUI session
|
|
1519
|
+
const currentSession = getCurrentSession();
|
|
1520
|
+
if (currentSession) {
|
|
1521
|
+
const hubWindowId = getHubWindowId(this.options.cwd);
|
|
1522
|
+
const spawnedWindows = getSpawnedWindows(this.options.cwd);
|
|
1523
|
+
const windows = listWindows(currentSession);
|
|
1524
|
+
|
|
1525
|
+
for (const window of windows) {
|
|
1526
|
+
// Skip the hub window - check by ID (stable) or name (fallback)
|
|
1527
|
+
if (window.id === hubWindowId || window.name === 'hub') continue;
|
|
1528
|
+
|
|
1529
|
+
// Only kill windows that were spawned by this TUI session
|
|
1530
|
+
if (!spawnedWindows.includes(window.name)) continue;
|
|
1531
|
+
|
|
1532
|
+
try {
|
|
1533
|
+
killWindow(currentSession, window.name);
|
|
1534
|
+
} catch (e) {
|
|
1535
|
+
// Log but don't fail - window might already be closed
|
|
1536
|
+
logTuiError('killWindow', e instanceof Error ? e : String(e), {
|
|
1537
|
+
session: currentSession,
|
|
1538
|
+
window: window.name,
|
|
1539
|
+
}, this.options.cwd);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
// Clear the TUI session state
|
|
1545
|
+
clearTuiSession(this.options.cwd);
|
|
1546
|
+
|
|
1547
|
+
try {
|
|
1548
|
+
this.screen.destroy();
|
|
1549
|
+
} finally {
|
|
1550
|
+
// Restore original output functions after cleanup
|
|
1551
|
+
// Delay restoration to catch any deferred terminal output
|
|
1552
|
+
const savedStdout = this.originalStdoutWrite;
|
|
1553
|
+
const savedStderr = this.originalStderrWrite;
|
|
1554
|
+
const savedConsoleLog = this.originalConsoleLog;
|
|
1555
|
+
const savedConsoleError = this.originalConsoleError;
|
|
1556
|
+
|
|
1557
|
+
setTimeout(() => {
|
|
1558
|
+
if (savedStdout) process.stdout.write = savedStdout;
|
|
1559
|
+
if (savedStderr) process.stderr.write = savedStderr;
|
|
1560
|
+
if (savedConsoleLog) console.log = savedConsoleLog;
|
|
1561
|
+
if (savedConsoleError) console.error = savedConsoleError;
|
|
1562
|
+
}, 100);
|
|
1563
|
+
}
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
public start(): void {
|
|
1567
|
+
this.screen.render();
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
export type { ActionItem } from './actions.js';
|
|
1572
|
+
export type { PromptItem } from './prompts-pane.js';
|
|
1573
|
+
export type { AgentInfo } from './status-pane.js';
|
|
1574
|
+
|