diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
index 4831225f9..d67bb525c 100644
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -1,316 +1,321 @@
 <?php
 
 /**
  * This file is automatically generated. Use 'phutil_mapper.php' to rebuild it.
  * @generated
  */
 
 phutil_register_library_map(array(
   'class' =>
   array(
+    'Aphront400Response' => 'aphront/response/400',
     'Aphront404Response' => 'aphront/response/404',
     'AphrontAjaxResponse' => 'aphront/response/ajax',
     'AphrontApplicationConfiguration' => 'aphront/applicationconfiguration',
     'AphrontController' => 'aphront/controller',
     'AphrontDatabaseConnection' => 'storage/connection/base',
     'AphrontDefaultApplicationConfiguration' => 'aphront/default/configuration',
     'AphrontDefaultApplicationController' => 'aphront/default/controller',
     'AphrontDialogResponse' => 'aphront/response/dialog',
     'AphrontDialogView' => 'view/dialog',
     'AphrontErrorView' => 'view/form/error',
     'AphrontException' => 'aphront/exception/base',
     'AphrontFileResponse' => 'aphront/response/file',
     'AphrontFormCheckboxControl' => 'view/form/control/checkbox',
     'AphrontFormControl' => 'view/form/control/base',
     'AphrontFormFileControl' => 'view/form/control/file',
     'AphrontFormMarkupControl' => 'view/form/control/markup',
     'AphrontFormSelectControl' => 'view/form/control/select',
     'AphrontFormStaticControl' => 'view/form/control/static',
     'AphrontFormSubmitControl' => 'view/form/control/submit',
     'AphrontFormTextAreaControl' => 'view/form/control/textarea',
     'AphrontFormTextControl' => 'view/form/control/text',
     'AphrontFormTokenizerControl' => 'view/form/control/tokenizer',
     'AphrontFormView' => 'view/form/base',
     'AphrontMySQLDatabaseConnection' => 'storage/connection/mysql',
     'AphrontNullView' => 'view/null',
     'AphrontPageView' => 'view/page/base',
     'AphrontPanelView' => 'view/layout/panel',
     'AphrontQueryConnectionException' => 'storage/exception/connection',
     'AphrontQueryConnectionLostException' => 'storage/exception/connectionlost',
     'AphrontQueryCountException' => 'storage/exception/count',
     'AphrontQueryException' => 'storage/exception/base',
     'AphrontQueryObjectMissingException' => 'storage/exception/objectmissing',
     'AphrontQueryParameterException' => 'storage/exception/parameter',
     'AphrontQueryRecoverableException' => 'storage/exception/recoverable',
     'AphrontRedirectException' => 'aphront/exception/redirect',
     'AphrontRedirectResponse' => 'aphront/response/redirect',
     'AphrontRequest' => 'aphront/request',
     'AphrontRequestFailureView' => 'view/page/failure',
     'AphrontResponse' => 'aphront/response/base',
     'AphrontSideNavView' => 'view/layout/sidenav',
     'AphrontTableView' => 'view/control/table',
     'AphrontURIMapper' => 'aphront/mapper',
     'AphrontView' => 'view/base',
     'AphrontWebpageResponse' => 'aphront/response/webpage',
     'CelerityAPI' => 'infratructure/celerity/api',
     'CelerityResourceController' => 'infratructure/celerity/controller',
     'CelerityResourceMap' => 'infratructure/celerity/map',
     'CelerityStaticResourceResponse' => 'infratructure/celerity/response',
     'ConduitAPIMethod' => 'applications/conduit/method/base',
     'ConduitAPIRequest' => 'applications/conduit/protocol/request',
     'ConduitAPI_conduit_connect_Method' => 'applications/conduit/method/conduit/connect',
     'ConduitAPI_differential_creatediff_Method' => 'applications/conduit/method/differential/creatediff',
     'ConduitAPI_differential_setdiffproperty_Method' => 'applications/conduit/method/differential/setdiffproperty',
     'ConduitAPI_file_upload_Method' => 'applications/conduit/method/file/upload',
     'ConduitAPI_user_find_Method' => 'applications/conduit/method/user/find',
     'ConduitException' => 'applications/conduit/protocol/exception',
     'DifferentialAction' => 'applications/differential/constants/action',
     'DifferentialAddCommentView' => 'applications/differential/view/addcomment',
     'DifferentialCCWelcomeMail' => 'applications/differential/mail/ccwelcome',
     'DifferentialChangeType' => 'applications/differential/constants/changetype',
     'DifferentialChangeset' => 'applications/differential/storage/changeset',
     'DifferentialChangesetDetailView' => 'applications/differential/view/changesetdetailview',
     'DifferentialChangesetListView' => 'applications/differential/view/changesetlistview',
     'DifferentialChangesetParser' => 'applications/differential/parser/changeset',
     'DifferentialChangesetViewController' => 'applications/differential/controller/changesetview',
     'DifferentialComment' => 'applications/differential/storage/comment',
+    'DifferentialCommentEditor' => 'applications/differential/editor/comment',
+    'DifferentialCommentMail' => 'applications/differential/mail/comment',
+    'DifferentialCommentSaveController' => 'applications/differential/controller/commentsave',
     'DifferentialController' => 'applications/differential/controller/base',
     'DifferentialDAO' => 'applications/differential/storage/base',
     'DifferentialDiff' => 'applications/differential/storage/diff',
     'DifferentialDiffContentMail' => 'applications/differential/mail/diffcontent',
     'DifferentialDiffProperty' => 'applications/differential/storage/diffproperty',
     'DifferentialDiffTableOfContentsView' => 'applications/differential/view/difftableofcontents',
     'DifferentialDiffViewController' => 'applications/differential/controller/diffview',
-    'DifferentialFeedbackMail' => 'applications/differential/mail/feedback',
     'DifferentialHunk' => 'applications/differential/storage/hunk',
     'DifferentialLintStatus' => 'applications/differential/constants/lintstatus',
     'DifferentialMail' => 'applications/differential/mail/base',
     'DifferentialNewDiffMail' => 'applications/differential/mail/newdiff',
     'DifferentialReviewRequestMail' => 'applications/differential/mail/reviewrequest',
     'DifferentialRevision' => 'applications/differential/storage/revision',
     'DifferentialRevisionCommentListView' => 'applications/differential/view/revisioncommentlist',
     'DifferentialRevisionCommentView' => 'applications/differential/view/revisioncomment',
     'DifferentialRevisionControlSystem' => 'applications/differential/constants/revisioncontrolsystem',
     'DifferentialRevisionDetailView' => 'applications/differential/view/revisiondetail',
     'DifferentialRevisionEditController' => 'applications/differential/controller/revisionedit',
     'DifferentialRevisionEditor' => 'applications/differential/editor/revision',
     'DifferentialRevisionListController' => 'applications/differential/controller/revisionlist',
     'DifferentialRevisionListData' => 'applications/differential/data/revisionlist',
     'DifferentialRevisionStatus' => 'applications/differential/constants/revisionstatus',
     'DifferentialRevisionUpdateHistoryView' => 'applications/differential/view/revisionupdatehistory',
     'DifferentialRevisionViewController' => 'applications/differential/controller/revisionview',
     'DifferentialUnitStatus' => 'applications/differential/constants/unitstatus',
     'Javelin' => 'infratructure/javelin/api',
     'LiskDAO' => 'storage/lisk/dao',
     'Phabricator404Controller' => 'applications/base/controller/404',
     'PhabricatorAuthController' => 'applications/auth/controlller/base',
     'PhabricatorConduitAPIController' => 'applications/conduit/controller/api',
     'PhabricatorConduitConnectionLog' => 'applications/conduit/storage/connectionlog',
     'PhabricatorConduitConsoleController' => 'applications/conduit/controller/console',
     'PhabricatorConduitController' => 'applications/conduit/controller/base',
     'PhabricatorConduitDAO' => 'applications/conduit/storage/base',
     'PhabricatorConduitLogController' => 'applications/conduit/controller/log',
     'PhabricatorConduitMethodCallLog' => 'applications/conduit/storage/methodcalllog',
     'PhabricatorController' => 'applications/base/controller/base',
     'PhabricatorDirectoryCategory' => 'applications/directory/storage/category',
     'PhabricatorDirectoryCategoryDeleteController' => 'applications/directory/controller/categorydelete',
     'PhabricatorDirectoryCategoryEditController' => 'applications/directory/controller/categoryedit',
     'PhabricatorDirectoryCategoryListController' => 'applications/directory/controller/categorylist',
     'PhabricatorDirectoryController' => 'applications/directory/controller/base',
     'PhabricatorDirectoryDAO' => 'applications/directory/storage/base',
     'PhabricatorDirectoryItem' => 'applications/directory/storage/item',
     'PhabricatorDirectoryItemDeleteController' => 'applications/directory/controller/itemdelete',
     'PhabricatorDirectoryItemEditController' => 'applications/directory/controller/itemedit',
     'PhabricatorDirectoryItemListController' => 'applications/directory/controller/itemlist',
     'PhabricatorDirectoryMainController' => 'applications/directory/controller/main',
     'PhabricatorFile' => 'applications/files/storage/file',
     'PhabricatorFileController' => 'applications/files/controller/base',
     'PhabricatorFileDAO' => 'applications/files/storage/base',
     'PhabricatorFileListController' => 'applications/files/controller/list',
     'PhabricatorFileStorageBlob' => 'applications/files/storage/storageblob',
     'PhabricatorFileURI' => 'applications/files/uri',
     'PhabricatorFileUploadController' => 'applications/files/controller/upload',
     'PhabricatorFileViewController' => 'applications/files/controller/view',
     'PhabricatorLiskDAO' => 'applications/base/storage/lisk',
     'PhabricatorLoginController' => 'applications/auth/controlller/login',
     'PhabricatorMailImplementationAdapter' => 'applications/metamta/adapter/base',
     'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'applications/metamta/adapter/phpmailerlite',
     'PhabricatorMetaMTAController' => 'applications/metamta/controller/base',
     'PhabricatorMetaMTADAO' => 'applications/metamta/storage/base',
     'PhabricatorMetaMTAListController' => 'applications/metamta/controller/list',
     'PhabricatorMetaMTAMail' => 'applications/metamta/storage/mail',
     'PhabricatorMetaMTAMailingList' => 'applications/metamta/storage/mailinglist',
     'PhabricatorMetaMTAMailingListEditController' => 'applications/metamta/controller/mailinglistedit',
     'PhabricatorMetaMTAMailingListsController' => 'applications/metamta/controller/mailinglists',
     'PhabricatorMetaMTASendController' => 'applications/metamta/controller/send',
     'PhabricatorMetaMTAViewController' => 'applications/metamta/controller/view',
     'PhabricatorObjectHandle' => 'applications/phid/handle',
     'PhabricatorObjectHandleData' => 'applications/phid/handle/data',
     'PhabricatorPHID' => 'applications/phid/storage/phid',
     'PhabricatorPHIDAllocateController' => 'applications/phid/controller/allocate',
     'PhabricatorPHIDController' => 'applications/phid/controller/base',
     'PhabricatorPHIDDAO' => 'applications/phid/storage/base',
     'PhabricatorPHIDListController' => 'applications/phid/controller/list',
     'PhabricatorPHIDLookupController' => 'applications/phid/controller/lookup',
     'PhabricatorPHIDType' => 'applications/phid/storage/type',
     'PhabricatorPHIDTypeEditController' => 'applications/phid/controller/typeedit',
     'PhabricatorPHIDTypeListController' => 'applications/phid/controller/typelist',
     'PhabricatorPeopleController' => 'applications/people/controller/base',
     'PhabricatorPeopleEditController' => 'applications/people/controller/edit',
     'PhabricatorPeopleListController' => 'applications/people/controller/list',
     'PhabricatorPeopleProfileController' => 'applications/people/controller/profile',
     'PhabricatorStandardPageView' => 'view/page/standard',
     'PhabricatorTypeaheadCommonDatasourceController' => 'applications/typeahead/controller/common',
     'PhabricatorTypeaheadDatasourceController' => 'applications/typeahead/controller/base',
     'PhabricatorUser' => 'applications/people/storage/user',
     'PhabricatorUserDAO' => 'applications/people/storage/base',
   ),
   'function' =>
   array(
     '_qsprintf_check_scalar_type' => 'storage/qsprintf',
     '_qsprintf_check_type' => 'storage/qsprintf',
     'celerity_generate_unique_node_id' => 'infratructure/celerity/api',
     'celerity_register_resource_map' => 'infratructure/celerity/map',
     'javelin_render_tag' => 'infratructure/javelin/markup',
     'qsprintf' => 'storage/qsprintf',
     'queryfx' => 'storage/queryfx',
     'queryfx_all' => 'storage/queryfx',
     'queryfx_one' => 'storage/queryfx',
     'require_celerity_resource' => 'infratructure/celerity/api',
     'vqsprintf' => 'storage/qsprintf',
     'vqueryfx' => 'storage/queryfx',
     'vqueryfx_all' => 'storage/queryfx',
     'xsprintf_query' => 'storage/qsprintf',
   ),
   'requires_class' =>
   array(
+    'Aphront400Response' => 'AphrontResponse',
     'Aphront404Response' => 'AphrontResponse',
     'AphrontAjaxResponse' => 'AphrontResponse',
     'AphrontDefaultApplicationConfiguration' => 'AphrontApplicationConfiguration',
     'AphrontDefaultApplicationController' => 'AphrontController',
     'AphrontDialogResponse' => 'AphrontResponse',
     'AphrontDialogView' => 'AphrontView',
     'AphrontErrorView' => 'AphrontView',
     'AphrontFileResponse' => 'AphrontResponse',
     'AphrontFormCheckboxControl' => 'AphrontFormControl',
     'AphrontFormControl' => 'AphrontView',
     'AphrontFormFileControl' => 'AphrontFormControl',
     'AphrontFormMarkupControl' => 'AphrontFormControl',
     'AphrontFormSelectControl' => 'AphrontFormControl',
     'AphrontFormStaticControl' => 'AphrontFormControl',
     'AphrontFormSubmitControl' => 'AphrontFormControl',
     'AphrontFormTextAreaControl' => 'AphrontFormControl',
     'AphrontFormTextControl' => 'AphrontFormControl',
     'AphrontFormTokenizerControl' => 'AphrontFormControl',
     'AphrontFormView' => 'AphrontView',
     'AphrontMySQLDatabaseConnection' => 'AphrontDatabaseConnection',
     'AphrontNullView' => 'AphrontView',
     'AphrontPageView' => 'AphrontView',
     'AphrontPanelView' => 'AphrontView',
     'AphrontQueryConnectionException' => 'AphrontQueryException',
     'AphrontQueryConnectionLostException' => 'AphrontQueryRecoverableException',
     'AphrontQueryCountException' => 'AphrontQueryException',
     'AphrontQueryObjectMissingException' => 'AphrontQueryException',
     'AphrontQueryParameterException' => 'AphrontQueryException',
     'AphrontQueryRecoverableException' => 'AphrontQueryException',
     'AphrontRedirectException' => 'AphrontException',
     'AphrontRedirectResponse' => 'AphrontResponse',
     'AphrontRequestFailureView' => 'AphrontView',
     'AphrontSideNavView' => 'AphrontView',
     'AphrontTableView' => 'AphrontView',
     'AphrontWebpageResponse' => 'AphrontResponse',
     'CelerityResourceController' => 'AphrontController',
     'ConduitAPI_conduit_connect_Method' => 'ConduitAPIMethod',
     'ConduitAPI_differential_creatediff_Method' => 'ConduitAPIMethod',
     'ConduitAPI_differential_setdiffproperty_Method' => 'ConduitAPIMethod',
     'ConduitAPI_file_upload_Method' => 'ConduitAPIMethod',
     'ConduitAPI_user_find_Method' => 'ConduitAPIMethod',
     'DifferentialAddCommentView' => 'AphrontView',
     'DifferentialCCWelcomeMail' => 'DifferentialReviewRequestMail',
     'DifferentialChangeset' => 'DifferentialDAO',
     'DifferentialChangesetDetailView' => 'AphrontView',
     'DifferentialChangesetListView' => 'AphrontView',
     'DifferentialChangesetViewController' => 'DifferentialController',
     'DifferentialComment' => 'DifferentialDAO',
+    'DifferentialCommentMail' => 'DifferentialMail',
+    'DifferentialCommentSaveController' => 'DifferentialController',
     'DifferentialController' => 'PhabricatorController',
     'DifferentialDAO' => 'PhabricatorLiskDAO',
     'DifferentialDiff' => 'DifferentialDAO',
     'DifferentialDiffContentMail' => 'DifferentialMail',
     'DifferentialDiffProperty' => 'DifferentialDAO',
     'DifferentialDiffTableOfContentsView' => 'AphrontView',
     'DifferentialDiffViewController' => 'DifferentialController',
-    'DifferentialFeedbackMail' => 'DifferentialMail',
     'DifferentialHunk' => 'DifferentialDAO',
     'DifferentialNewDiffMail' => 'DifferentialReviewRequestMail',
     'DifferentialReviewRequestMail' => 'DifferentialMail',
     'DifferentialRevision' => 'DifferentialDAO',
     'DifferentialRevisionCommentListView' => 'AphrontView',
     'DifferentialRevisionCommentView' => 'AphrontView',
     'DifferentialRevisionDetailView' => 'AphrontView',
     'DifferentialRevisionEditController' => 'DifferentialController',
     'DifferentialRevisionListController' => 'DifferentialController',
     'DifferentialRevisionUpdateHistoryView' => 'AphrontView',
     'DifferentialRevisionViewController' => 'DifferentialController',
     'Phabricator404Controller' => 'PhabricatorController',
     'PhabricatorAuthController' => 'PhabricatorController',
     'PhabricatorConduitAPIController' => 'PhabricatorConduitController',
     'PhabricatorConduitConnectionLog' => 'PhabricatorConduitDAO',
     'PhabricatorConduitConsoleController' => 'PhabricatorConduitController',
     'PhabricatorConduitController' => 'PhabricatorController',
     'PhabricatorConduitDAO' => 'PhabricatorLiskDAO',
     'PhabricatorConduitLogController' => 'PhabricatorConduitController',
     'PhabricatorConduitMethodCallLog' => 'PhabricatorConduitDAO',
     'PhabricatorController' => 'AphrontController',
     'PhabricatorDirectoryCategory' => 'PhabricatorDirectoryDAO',
     'PhabricatorDirectoryCategoryDeleteController' => 'PhabricatorDirectoryController',
     'PhabricatorDirectoryCategoryEditController' => 'PhabricatorDirectoryController',
     'PhabricatorDirectoryCategoryListController' => 'PhabricatorDirectoryController',
     'PhabricatorDirectoryController' => 'PhabricatorController',
     'PhabricatorDirectoryDAO' => 'PhabricatorLiskDAO',
     'PhabricatorDirectoryItem' => 'PhabricatorDirectoryDAO',
     'PhabricatorDirectoryItemDeleteController' => 'PhabricatorDirectoryController',
     'PhabricatorDirectoryItemEditController' => 'PhabricatorDirectoryController',
     'PhabricatorDirectoryItemListController' => 'PhabricatorDirectoryController',
     'PhabricatorDirectoryMainController' => 'PhabricatorDirectoryController',
     'PhabricatorFile' => 'PhabricatorFileDAO',
     'PhabricatorFileController' => 'PhabricatorController',
     'PhabricatorFileDAO' => 'PhabricatorLiskDAO',
     'PhabricatorFileListController' => 'PhabricatorFileController',
     'PhabricatorFileStorageBlob' => 'PhabricatorFileDAO',
     'PhabricatorFileUploadController' => 'PhabricatorFileController',
     'PhabricatorFileViewController' => 'PhabricatorFileController',
     'PhabricatorLiskDAO' => 'LiskDAO',
     'PhabricatorLoginController' => 'PhabricatorAuthController',
     'PhabricatorMailImplementationPHPMailerLiteAdapter' => 'PhabricatorMailImplementationAdapter',
     'PhabricatorMetaMTAController' => 'PhabricatorController',
     'PhabricatorMetaMTADAO' => 'PhabricatorLiskDAO',
     'PhabricatorMetaMTAListController' => 'PhabricatorMetaMTAController',
     'PhabricatorMetaMTAMail' => 'PhabricatorMetaMTADAO',
     'PhabricatorMetaMTAMailingList' => 'PhabricatorMetaMTADAO',
     'PhabricatorMetaMTAMailingListEditController' => 'PhabricatorMetaMTAController',
     'PhabricatorMetaMTAMailingListsController' => 'PhabricatorMetaMTAController',
     'PhabricatorMetaMTASendController' => 'PhabricatorMetaMTAController',
     'PhabricatorMetaMTAViewController' => 'PhabricatorMetaMTAController',
     'PhabricatorPHID' => 'PhabricatorPHIDDAO',
     'PhabricatorPHIDAllocateController' => 'PhabricatorPHIDController',
     'PhabricatorPHIDController' => 'PhabricatorController',
     'PhabricatorPHIDDAO' => 'PhabricatorLiskDAO',
     'PhabricatorPHIDListController' => 'PhabricatorPHIDController',
     'PhabricatorPHIDLookupController' => 'PhabricatorPHIDController',
     'PhabricatorPHIDType' => 'PhabricatorPHIDDAO',
     'PhabricatorPHIDTypeEditController' => 'PhabricatorPHIDController',
     'PhabricatorPHIDTypeListController' => 'PhabricatorPHIDController',
     'PhabricatorPeopleController' => 'PhabricatorController',
     'PhabricatorPeopleEditController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleListController' => 'PhabricatorPeopleController',
     'PhabricatorPeopleProfileController' => 'PhabricatorPeopleController',
     'PhabricatorStandardPageView' => 'AphrontPageView',
     'PhabricatorTypeaheadCommonDatasourceController' => 'PhabricatorTypeaheadDatasourceController',
     'PhabricatorTypeaheadDatasourceController' => 'PhabricatorController',
     'PhabricatorUser' => 'PhabricatorUserDAO',
     'PhabricatorUserDAO' => 'PhabricatorLiskDAO',
   ),
   'requires_interface' =>
   array(
   ),
 ));
diff --git a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php
index ee7c71caa..4344d37c7 100644
--- a/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php
+++ b/src/aphront/default/configuration/AphrontDefaultApplicationConfiguration.php
@@ -1,178 +1,186 @@
 <?php
 
 /*
  * Copyright 2011 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 /**
  * @group aphront
  */
 class AphrontDefaultApplicationConfiguration
   extends AphrontApplicationConfiguration {
 
   public function getApplicationName() {
     return 'aphront-default';
   }
 
   public function getURIMap() {
     return array(
       '/repository/' => array(
         '$'                     => 'RepositoryListController',
         'new/$'                 => 'RepositoryEditController',
         'edit/(?<id>\d+)/$'     => 'RepositoryEditController',
         'delete/(?<id>\d+)/$'   => 'RepositoryDeleteController',
       ),
       '/' => array(
         '$'                     => 'PhabricatorDirectoryMainController',
       ),
       '/directory/' => array(
         'item/$'
           => 'PhabricatorDirectoryItemListController',
         'item/edit/(?:(?<id>\d+)/)?$'
           => 'PhabricatorDirectoryItemEditController',
         'item/delete/(?<id>\d+)/'
           => 'PhabricatorDirectoryItemDeleteController',
         'category/$'
           => 'PhabricatorDirectoryCategoryListController',
         'category/edit/(?:(?<id>\d+)/)?$'
           => 'PhabricatorDirectoryCategoryEditController',
         'category/delete/(?<id>\d+)/'
           => 'PhabricatorDirectoryCategoryDeleteController',
       ),
       '/file/' => array(
         '$' => 'PhabricatorFileListController',
         'upload/$' => 'PhabricatorFileUploadController',
         '(?<view>info)/(?<phid>[^/]+)/' => 'PhabricatorFileViewController',
         '(?<view>view)/(?<phid>[^/]+)/' => 'PhabricatorFileViewController',
         '(?<view>download)/(?<phid>[^/]+)/' => 'PhabricatorFileViewController',
       ),
       '/phid/' => array(
         '$' => 'PhabricatorPHIDLookupController',
         'list/$' => 'PhabricatorPHIDListController',
         'type/$' => 'PhabricatorPHIDTypeListController',
         'type/edit/(?:(?<id>\d+)/)?$' => 'PhabricatorPHIDTypeEditController',
         'new/$' => 'PhabricatorPHIDAllocateController',
       ),
       '/people/' => array(
         '$' => 'PhabricatorPeopleListController',
         'edit/(?:(?<username>\w+)/)?$' => 'PhabricatorPeopleEditController',
       ),
       '/p/(?<username>\w+)/$' => 'PhabricatorPeopleProfileController',
       '/conduit/' => array(
         '$' => 'PhabricatorConduitConsoleController',
         'method/(?<method>[^/]+)$' => 'PhabricatorConduitConsoleController',
         'log/$' => 'PhabricatorConduitLogController',
       ),
       '/api/(?<method>[^/]+)$' => 'PhabricatorConduitAPIController',
 
 
       '/D(?<id>\d+)' => 'DifferentialRevisionViewController',
       '/differential/' => array(
         '$' => 'DifferentialRevisionListController',
         'filter/(?<filter>\w+)/$' => 'DifferentialRevisionListController',
         'diff/(?<id>\d+)/$'       => 'DifferentialDiffViewController',
         'changeset/(?<id>\d+)/$'  => 'DifferentialChangesetViewController',
         'revision/edit/(?:(?<id>\d+)/)?$'
           => 'DifferentialRevisionEditController',
+        'comment/' => array(
+          'preview/$' => 'DifferentialCommentPreviewController',
+          'save/$' => 'DifferentialCommentSaveController',
+          'inline/' => array(
+            'preview/$' => 'DifferentialInlineCommentPreviewController',
+            'edit/$' => 'DifferentialInlineCommentEditController',
+          ),
+        ),
       ),
 
       '/res/' => array(
         '(?<package>pkg/)?(?<hash>[a-f0-9]{8})/(?<path>.+\.(?:css|js))$'
           => 'CelerityResourceController',
       ),
 
       '/typeahead/' => array(
         'common/(?<type>\w+)/$'
           => 'PhabricatorTypeaheadCommonDatasourceController',
       ),
 
       '/mail/' => array(
         '$' => 'PhabricatorMetaMTAListController',
         'send/$' => 'PhabricatorMetaMTASendController',
         'view/(?<id>\d+)/$' => 'PhabricatorMetaMTAViewController',
         'lists/$' => 'PhabricatorMetaMTAMailingListsController',
         'lists/edit/(?:(?<id>\d+)/)?$'
           => 'PhabricatorMetaMTAMailingListEditController',
       ),
 
       '/login/' => 'PhabricatorLoginController',
     );
   }
 
   public function buildRequest() {
     $request = new AphrontRequest($this->getHost(), $this->getPath());
     $request->setRequestData($_GET + $_POST);
     return $request;
   }
 
   public function handleException(Exception $ex) {
 
     $class    = phutil_escape_html(get_class($ex));
     $message  = phutil_escape_html($ex->getMessage());
 
     $content =
       '<div class="aphront-unhandled-exception">'.
         '<h1>Unhandled Exception "'.$class.'": '.$message.'</h1>'.
         '<code>'.phutil_escape_html((string)$ex).'</code>'.
       '</div>';
 
     $view = new PhabricatorStandardPageView();
     $view->appendChild($content);
 
     $response = new AphrontWebpageResponse();
     $response->setContent($view->render());
 
     return $response;
   }
 
   public function willSendResponse(AphrontResponse $response) {
     $request = $this->getRequest();
     if ($response instanceof AphrontDialogResponse) {
       if (!$request->isAjax()) {
         $view = new PhabricatorStandardPageView();
         $view->appendChild(
           '<div style="padding: 2em 0;">'.
             $response->buildResponseString().
           '</div>');
         $response = new AphrontWebpageResponse();
         $response->setContent($view->render());
         return $response;
       }
     } else if ($response instanceof Aphront404Response) {
 
       $failure = new AphrontRequestFailureView();
       $failure->setHeader('404 Not Found');
       $failure->appendChild(
         '<p>The page you requested was not found.</p>');
 
       $view = new PhabricatorStandardPageView();
       $view->setTitle('404 Not Found');
       $view->appendChild($failure);
 
       $response = new AphrontWebpageResponse();
       $response->setContent($view->render());
       $response->setHTTPResponseCode(404);
       return $response;
     }
 
     return $response;
   }
 
   public function build404Controller() {
     return array(new Phabricator404Controller($this->getRequest()), array());
   }
 
 
 }
diff --git a/src/aphront/response/400/Aphront400Response.php b/src/aphront/response/400/Aphront400Response.php
new file mode 100644
index 000000000..46ecbe095
--- /dev/null
+++ b/src/aphront/response/400/Aphront400Response.php
@@ -0,0 +1,28 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @group aphront
+ */
+class Aphront400Response extends AphrontResponse {
+
+  public function buildResponseString() {
+    return '400 Bad Request';
+  }
+
+}
diff --git a/src/aphront/response/400/__init__.php b/src/aphront/response/400/__init__.php
new file mode 100644
index 000000000..8bc616ae0
--- /dev/null
+++ b/src/aphront/response/400/__init__.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * This file is automatically generated. Lint this module to rebuild it.
+ * @generated
+ */
+
+
+
+phutil_require_module('phabricator', 'aphront/response/base');
+
+
+phutil_require_source('Aphront400Response.php');
diff --git a/src/applications/differential/controller/commentsave/DifferentialCommentSaveController.php b/src/applications/differential/controller/commentsave/DifferentialCommentSaveController.php
new file mode 100644
index 000000000..b534e4353
--- /dev/null
+++ b/src/applications/differential/controller/commentsave/DifferentialCommentSaveController.php
@@ -0,0 +1,56 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class DifferentialCommentSaveController extends DifferentialController {
+
+  public function processRequest() {
+    $request = $this->getRequest();
+    if (!$request->isFormPost()) {
+      return new Aphront400Response();
+    }
+
+    $revision_id = $request->getInt('revision_id');
+    $revision = id(new DifferentialRevision())->load($revision_id);
+    if (!$revision) {
+      return new Aphront400Response();
+    }
+
+    $comment    = $request->getStr('comment');
+    $action     = $request->getStr('action');
+    $reviewers  = $request->getStr('reviewers');
+
+    $editor = new DifferentialCommentEditor(
+      $revision,
+      $request->getUser()->getPHID(),
+      $action);
+
+    $editor
+      ->setMessage($comment)
+      ->setAttachInlineComments(true)
+      ->setAddCC($action != DifferentialAction::ACTION_RESIGN)
+      ->setAddedReviewers($reviewers)
+      ->save();
+
+    // TODO: Diff change detection?
+    // TODO: Clear draft
+
+    return id(new AphrontRedirectResponse())
+      ->setURI('/D'.$revision->getID());
+  }
+
+}
diff --git a/src/applications/differential/mail/feedback/__init__.php b/src/applications/differential/controller/commentsave/__init__.php
similarity index 50%
copy from src/applications/differential/mail/feedback/__init__.php
copy to src/applications/differential/controller/commentsave/__init__.php
index dd323e630..d7c03ad62 100644
--- a/src/applications/differential/mail/feedback/__init__.php
+++ b/src/applications/differential/controller/commentsave/__init__.php
@@ -1,14 +1,18 @@
 <?php
 /**
  * This file is automatically generated. Lint this module to rebuild it.
  * @generated
  */
 
 
 
-phutil_require_module('phabricator', 'applications/differential/constants/action');
-phutil_require_module('phabricator', 'applications/differential/constants/revisionstatus');
-phutil_require_module('phabricator', 'applications/differential/mail/base');
+phutil_require_module('phabricator', 'aphront/response/400');
+phutil_require_module('phabricator', 'aphront/response/redirect');
+phutil_require_module('phabricator', 'applications/differential/controller/base');
+phutil_require_module('phabricator', 'applications/differential/editor/comment');
+phutil_require_module('phabricator', 'applications/differential/storage/revision');
 
+phutil_require_module('phutil', 'utils');
 
-phutil_require_source('DifferentialFeedbackMail.php');
+
+phutil_require_source('DifferentialCommentSaveController.php');
diff --git a/src/applications/differential/controller/revisionview/DifferentialRevisionViewController.php b/src/applications/differential/controller/revisionview/DifferentialRevisionViewController.php
index ba28a982a..1e55f8c82 100644
--- a/src/applications/differential/controller/revisionview/DifferentialRevisionViewController.php
+++ b/src/applications/differential/controller/revisionview/DifferentialRevisionViewController.php
@@ -1,1727 +1,1728 @@
 <?php
 
 /*
  * Copyright 2011 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 class DifferentialRevisionViewController extends DifferentialController {
 
   private $revisionID;
 
   public function willProcessRequest(array $data) {
     $this->revisionID = $data['id'];
   }
 
   public function processRequest() {
 
     $request = $this->getRequest();
 
     $revision = id(new DifferentialRevision())->load($this->revisionID);
     if (!$revision) {
       return new Aphront404Response();
     }
 
     $revision->loadRelationships();
 
     $diffs = $revision->loadDiffs();
 
     $target = end($diffs);
 
     $changesets = $target->loadChangesets();
 
     $comments = $revision->loadComments();
     $comments = array_merge(
       $this->getImplicitComments($revision),
       $comments);
 
     $object_phids = array_merge(
       $revision->getReviewers(),
       $revision->getCCPHIDs(),
       array(
         $revision->getAuthorPHID(),
         $request->getUser()->getPHID(),
       ),
       mpull($comments, 'getAuthorPHID'));
 
     $handles = id(new PhabricatorObjectHandleData($object_phids))
       ->loadHandles();
 
     $revision_detail = new DifferentialRevisionDetailView();
     $revision_detail->setRevision($revision);
 
     $properties = $this->getRevisionProperties($revision, $target, $handles);
     $revision_detail->setProperties($properties);
 
     $actions = $this->getRevisionActions($revision);
     $revision_detail->setActions($actions);
 
     $comment_view = new DifferentialRevisionCommentListView();
     $comment_view->setComments($comments);
     $comment_view->setHandles($handles);
 
     $diff_history = new DifferentialRevisionUpdateHistoryView();
     $diff_history->setDiffs($diffs);
 
     $toc_view = new DifferentialDiffTableOfContentsView();
     $toc_view->setChangesets($changesets);
 
     $changeset_view = new DifferentialChangesetListView();
     $changeset_view->setChangesets($changesets);
 
     $comment_form = new DifferentialAddCommentView();
     $comment_form->setRevision($revision);
     $comment_form->setActions($this->getRevisionCommentActions($revision));
+    $comment_form->setActionURI('/differential/comment/save/');
 
     return $this->buildStandardPageResponse(
       '<div class="differential-primary-pane">'.
         $revision_detail->render().
         $comment_view->render().
         $diff_history->render().
         $toc_view->render().
         $changeset_view->render().
         $comment_form->render().
       '</div>',
       array(
         'title' => $revision->getTitle(),
       ));
   }
 
   private function getImplicitComments(DifferentialRevision $revision) {
 
     $template = new DifferentialComment();
     $template->setAuthorPHID($revision->getAuthorPHID());
     $template->setRevisionID($revision->getID());
     $template->setDateCreated($revision->getDateCreated());
 
     $comments = array();
 
     if (strlen($revision->getSummary())) {
       $summary_comment = clone $template;
       $summary_comment->setContent($revision->getSummary());
       $summary_comment->setAction(DifferentialAction::ACTION_SUMMARIZE);
       $comments[] = $summary_comment;
     }
 
     if (strlen($revision->getTestPlan())) {
       $testplan_comment = clone $template;
       $testplan_comment->setContent($revision->getTestPlan());
       $testplan_comment->setAction(DifferentialAction::ACTION_TESTPLAN);
       $comments[] = $testplan_comment;
     }
 
     return $comments;
   }
 
   private function getRevisionProperties(
     DifferentialRevision $revision,
     DifferentialDiff $diff,
     array $handles) {
 
     $properties = array();
 
     $status = $revision->getStatus();
     $status = DifferentialRevisionStatus::getNameForRevisionStatus($status);
     $properties['Revision Status'] = '<strong>'.$status.'</strong>';
 
     $author = $handles[$revision->getAuthorPHID()];
     $properties['Author'] = $author->renderLink();
 
     $properties['Reviewers'] = $this->renderHandleLinkList(
       array_select_keys(
         $handles,
         $revision->getReviewers()));
 
     $properties['CCs'] = $this->renderHandleLinkList(
       array_select_keys(
         $handles,
         $revision->getCCPHIDs()));
 
     $path = $diff->getSourcePath();
     if ($path) {
       $branch = $diff->getBranch() ? ' (' . $diff->getBranch() . ')' : '';
       $host = $diff->getSourceMachine();
       if ($host) {
         $host .= ':';
       }
       $properties['Path'] = phutil_escape_html("{$host}{$path} {$branch}");
     }
 
 
     $properties['Lint'] = 'TODO';
     $properties['Unit'] = 'TODO';
 
     return $properties;
   }
 
   private function getRevisionActions(DifferentialRevision $revision) {
     $viewer_phid = $this->getRequest()->getUser()->getPHID();
     $viewer_is_owner = ($revision->getAuthorPHID() == $viewer_phid);
     $viewer_is_reviewer = in_array($viewer_phid, $revision->getReviewers());
     $viewer_is_cc = in_array($viewer_phid, $revision->getCCPHIDs());
     $status = $revision->getStatus();
     $revision_id = $revision->getID();
     $revision_phid = $revision->getPHID();
 
     $links = array();
 
     if ($viewer_is_owner) {
       $links[] = array(
         'class' => 'revision-edit',
         'href'  => "/differential/revision/edit/{$revision_id}/",
         'name'  => 'Edit Revision',
       );
     }
 
     if (!$viewer_is_owner && !$viewer_is_reviewer) {
       $action = $viewer_is_cc ? 'rem' : 'add';
       $links[] = array(
         'class' => $viewer_is_cc ? 'subscribe-rem' : 'subscribe-add',
         'href'  => "/differential/subscribe/{$action}/{$revision_id}/",
         'name'  => $viewer_is_cc ? 'Unsubscribe' : 'Subscribe',
       );
     } else {
       $links[] = array(
         'class' => 'subscribe-rem unavailable',
         'name'  => 'Automatically Subscribed',
       );
     }
 
     $links[] = array(
       'class' => 'transcripts-metamta',
       'name'  => 'MetaMTA Transcripts',
       'href'  => "/mail/?phid={$revision_phid}",
     );
 
     return $links;
   }
 
 
   private function renderHandleLinkList(array $list) {
     if (empty($list)) {
       return '<em>None</em>';
     }
     return implode(', ', mpull($list, 'renderLink'));
   }
 
   private function getRevisionCommentActions(DifferentialRevision $revision) {
 
     $actions = array(
       DifferentialAction::ACTION_COMMENT => true,
     );
 
     $viewer_phid = $this->getRequest()->getUser()->getPHID();
     $viewer_is_owner = ($viewer_phid == $revision->getAuthorPHID());
 
     if ($viewer_is_owner) {
       switch ($revision->getStatus()) {
         case DifferentialRevisionStatus::NEEDS_REVIEW:
           $actions[DifferentialAction::ACTION_ABANDON] = true;
           break;
         case DifferentialRevisionStatus::NEEDS_REVISION:
         case DifferentialRevisionStatus::ACCEPTED:
           $actions[DifferentialAction::ACTION_ABANDON] = true;
           $actions[DifferentialAction::ACTION_REQUEST] = true;
           break;
         case DifferentialRevisionStatus::COMMITTED:
           break;
         case DifferentialRevisionStatus::ABANDONED:
           $actions[DifferentialAction::ACTION_RECLAIM] = true;
           break;
       }
     } else {
       switch ($revision->getStatus()) {
         case DifferentialRevisionStatus::NEEDS_REVIEW:
           $actions[DifferentialAction::ACTION_ACCEPT] = true;
           $actions[DifferentialAction::ACTION_REJECT] = true;
           break;
         case DifferentialRevisionStatus::NEEDS_REVISION:
           $actions[DifferentialAction::ACTION_ACCEPT] = true;
           break;
         case DifferentialRevisionStatus::ACCEPTED:
           $actions[DifferentialAction::ACTION_REJECT] = true;
           break;
         case DifferentialRevisionStatus::COMMITTED:
         case DifferentialRevisionStatus::ABANDONED:
           break;
       }
     }
 
     $actions[DifferentialAction::ACTION_ADDREVIEWERS] = true;
 
     return array_keys($actions);
   }
 
 }
 /*
 
 
   protected function getRevisionActions(DifferentialRevision $revision) {
 
     $viewer_id = $this->getRequest()->getViewerContext()->getUserID();
     $viewer_is_owner = ($viewer_id == $revision->getOwnerID());
     $viewer_is_reviewer =
       ((array_search($viewer_id, $revision->getReviewers())) !== false);
     $viewer_is_cc =
       ((array_search($viewer_id, $revision->getCCFBIDs())) !== false);
     $status = $revision->getStatus();
 
     $links = array();
 
     if (!$viewer_is_owner && !$viewer_is_reviewer) {
       $action = $viewer_is_cc
         ? 'rem'
         : 'add';
       $revision_id = $revision->getID();
       $href = "/differential/subscribe/{$action}/{$revision_id}";
       $links[] = array(
         $viewer_is_cc ? 'subscribe-disabled' : 'subscribe-enabled',
         <a href={$href}>{$viewer_is_cc ? 'Unsubscribe' : 'Subscribe'}</a>,
       );
     } else {
       $links[] = array(
         'subscribe-disabled unavailable',
         <a>Automatically Subscribed</a>,
       );
     }
 
     $blast_uri = RedirectURI(
       '/intern/differential/?action=tasks&fbid='.$revision->getFBID())
       ->setTier('intern');
     $links[] = array(
       'tasks',
       <a href={$blast_uri}>Edit Tasks</a>,
     );
 
     $engineering_repository_id = RepositoryRef::getByCallsign('E')->getID();
     $svn_revision = $revision->getSVNRevision();
     if ($status == DifferentialConstants::COMMITTED &&
         $svn_revision &&
         $revision->getRepositoryID() == $engineering_repository_id) {
       $href = '/intern/push/request.php?rev='.$svn_revision;
       $href = RedirectURI($href)->setTier('intern');
       $links[] = array(
         'merge',
         <a href={$href} id="ask_for_merge_link">Ask for Merge</a>,
       );
     }
 
     $links[] = array(
       'herald-transcript',
       <a href={"/herald/transcript/?fbid=".$revision->getFBID()}
         >Herald Transcripts</a>,
     );
     $links[] = array(
       'metamta-transcript',
       <a href={"/mail/?view=all&fbid=".$revision->getFBID()}
         >MetaMTA Transcripts</a>,
     );
 
 
     $list = <ul class="differential-actions" />;
     foreach ($links as $link) {
       list($class, $tag) = $link;
       $list->appendChild(<li class={$class}>{$tag}</li>);
     }
 
     return $list;
 
 
 
 /*
 // TODO
 //    $sandcastle = $this->getSandcastleURI($diff);
 //    if ($sandcastle) {
 //      $fields['Sandcastle'] = <a href={$sandcastle}>{$sandcastle}</a>;
 //    }
 
     $path = $diff->getSourcePath();
     if ($path) {
       $host = $diff->getSourceMachine();
       $branch = $diff->getGitBranch() ? ' (' . $diff->getGitBranch() . ')' : '';
 
       if ($host) {
 // TODO
 //        $user = $handles[$this->getRequest()->getViewerContext()->getUserID()]
 //          ->getName();
         $user = 'TODO';
         $fields['Path'] =
           <x:frag>
             <a href={"ssh://{$user}@{$host}"}>{$host}</a>:{$path}{$branch}
           </x:frag>;
       } else {
         $fields['Path'] = $path;
       }
     }
 
     $reviewer_links = array();
     foreach ($revision->getReviewers() as $reviewer) {
       $reviewer_links[] = <tools:handle handle={$handles[$reviewer]}
                                           link={true} />;
     }
     if ($reviewer_links) {
       $fields['Reviewers'] = array_implode(', ', $reviewer_links);
     } else {
       $fields['Reviewers'] = <em>None</em>;
     }
 
     $ccs = $revision->getCCFBIDs();
     if ($ccs) {
       $links = array();
       foreach ($ccs as $cc) {
         $links[] = <tools:handle handle={$handles[$cc]}
                                    link={true} />;
       }
       $fields['CCs'] = array_implode(', ', $links);
     }
 
     $blame_rev = $revision->getSvnBlameRevision();
     if ($blame_rev) {
       if ($revision->getRepositoryRef() && is_numeric($blame_rev)) {
         $ref = new RevisionRef($revision->getRepositoryRef(), $blame_rev);
         $fields['Blame Revision'] =
           <a href={URI($ref->getDetailURL())}>
             {$ref->getName()}
           </a>;
       } else {
         $fields['Blame Revision'] = $blame_rev;
       }
     }
 
     $tasks = $revision->getTaskHandles();
 
     if ($tasks) {
       $links = array();
       foreach ($tasks as $task) {
         $links[] = <tools:handle handle={$task} link={true} />;
       }
       $fields['Tasks'] = array_implode(<br />, $links);
     }
 
     $bugzilla_id = $revision->getBugzillaID();
     if ($bugzilla_id) {
       $href = 'http://bugs.developers.facebook.com/show_bug.cgi?id='.
         $bugzilla_id;
       $fields['Bugzilla'] = <a href={$href}>{'#'.$bugzilla_id}</a>;
     }
 
     $fields['Apply Patch'] = <tt>arc patch --revision {$revision->getID()}</tt>;
 
     if ($diff->getParentRevisionID()) {
       $parent = id(new DifferentialRevision())->load(
         $diff->getParentRevisionID());
       if ($parent) {
         $fields['Depends On'] =
           <a href={$parent->getURI()}>
             D{$parent->getID()}: {$parent->getName()}
           </a>;
       }
     }
 
     $star = <span class="star">{"\xE2\x98\x85"}</span>;
 
     Javelin::initBehavior('differential-star-more');
 
     switch ($diff->getLinted()) {
       case Diff::LINT_FAIL:
         $more = $this->renderDiffPropertyMoreLink($diff, 'lint');
         $fields['Lint'] =
           <x:frag>
             <span class="star-warn">{$star} Lint Failures</span>
             {$more}
           </x:frag>;
         break;
       case Diff::LINT_WARNINGS:
         $more = $this->renderDiffPropertyMoreLink($diff, 'lint');
         $fields['Lint'] =
           <x:frag>
             <span class="star-warn">{$star} Lint Warnings</span>
             {$more}
           </x:frag>;
         break;
       case Diff::LINT_OKAY:
         $fields['Lint'] =
           <span class="star-okay">{$star} Lint Free</span>;
         break;
       default:
       case Diff::LINT_NO:
         $fields['Lint'] =
           <span class="star-none">{$star} Not Linted</span>;
         break;
     }
 
     $unit_details = false;
     switch ($diff->getUnitTested()) {
       case Diff::UNIT_FAIL:
         $fields['Unit Tests'] =
             <span class="star-warn">{$star} Unit Test Failures</span>;
         $unit_details = true;
         break;
       case Diff::UNIT_WARN:
         $fields['Unit Tests'] =
             <span class="star-warn">{$star} Unit Test Warnings</span>;
         $unit_details = true;
         break;
       case Diff::UNIT_OKAY:
         $fields['Unit Tests'] =
           <span class="star-okay">{$star} Unit Tests Passed</span>;
         $unit_details = true;
         break;
       case Diff::UNIT_NO_TESTS:
         $fields['Unit Tests'] =
           <span class="star-none">{$star} No Test Coverage</span>;
         break;
       case Diff::UNIT_NO:
       default:
         $fields['Unit Tests'] =
           <span class="star-none">{$star} Not Unit Tested</span>;
         break;
     }
 
     if ($unit_details) {
       $fields['Unit Tests'] =
         <x:frag>
           {$fields['Unit Tests']}
           {$this->renderDiffPropertyMoreLink($diff, 'unit')}
         </x:frag>;
     }
 
     $platform_impact = $revision->getPlatformImpact();
     if ($platform_impact) {
       $fields['Platform Impact'] =
         <text linebreaks="true">{$platform_impact}</text>;
     }
 
     return $fields;
   }
 
 
 }
 
 /*
 
 
 
   protected function getSandcastleURI(Diff $diff) {
     $uri = $this->getDiffProperty($diff, 'facebook:sandcastle_uri');
     if (!$uri) {
       $uri = $diff->getSandboxURL();
     }
     return $uri;
   }
 
   protected function getDiffProperty(Diff $diff, $property, $default = null) {
     $diff_id = $diff->getID();
     if (empty($this->diffProperties[$diff_id])) {
       $props = id(new DifferentialDiffProperty())
         ->loadAllWhere('diffID = %s', $diff_id);
       $dict = array_pull($props, 'getData', 'getName');
       $this->diffProperties[$diff_id] = $dict;
     }
     return idx($this->diffProperties[$diff_id], $property, $default);
   }
 
   public function process() {
     $uri = $this->getRequest()->getPath();
     if (starts_with($uri, '/d')) {
       return <alite:redirect uri={strtoupper($uri)}/>;
     }
 
     $revision = id(new DifferentialRevision())->load($this->revisionID);
     if (!$revision) {
       throw new Exception("Bad revision ID.");
     }
 
     $diffs = id(new Diff())->loadAllWhere(
       'revisionID = %d',
       $revision->getID());
     $diffs = array_psort($diffs, 'getID');
 
     $request = $this->getRequest();
     $new = $request->getInt('new');
     $old = $request->getInt('old');
 
     if (($new || $old) && $new <= $old) {
       throw new Exception(
         "You can only view the diff of an older update relative to a newer ".
         "update.");
     }
 
     if ($new && empty($diffs[$new])) {
       throw new Exception(
         "The 'new' diff does not exist.");
     } else if ($new) {
       $diff = $diffs[$new];
     } else {
       $diff = end($diffs);
       if (!$diff) {
         throw new Exception("No diff attached to this revision?");
       }
       $new = $diff->getID();
     }
 
     $target_diff = $diff;
 
     if ($old && empty($diffs[$old])) {
       throw new Exception(
         "The 'old' diff does not exist.");
     }
 
     $rows = array(array('Base', '', true, false, null,
       $diff->getSourceControlBaseRevision()
         ? $diff->getSourceControlBaseRevision()
         : <em>Master</em>));
     $idx = 0;
     foreach ($diffs as $cdiff) {
       $rows[] = array(
         'Diff '.(++$idx),
         $cdiff->getID(),
         $cdiff->getID() != max(array_pull($diffs, 'getID')),
         true,
         $cdiff->getDateCreated(),
         $cdiff->getDescription()
           ? $cdiff->getDescription()
           : <em>No description available.</em>,
         $cdiff->getUnitTested(),
         $cdiff->getLinted());
     }
 
     $diff_table =
       <table class="differential-diff-differ">
         <tr>
           <th>Diff</th>
           <th>Diff ID</th>
           <th>Description</th>
           <th>Age</th>
           <th>Lint</th>
           <th>Unit</th>
         </tr>
       </table>;
     $ii = 0;
 
     $old_ids = array();
     foreach ($rows as $row) {
       $xold = null;
       if ($row[2]) {
         $lradio = <input name="old" value={$row[1]} type="radio"
           disabled={$row[1] >= $new}
           checked={$old == $row[1]} />;
         if ($old == $row[1]) {
           $xold = 'old-now';
         }
         $old_ids[] = $lradio->requireUniqueID();
       } else {
         $lradio = null;
       }
       $xnew = null;
       if ($row[3]) {
         $rradio = <input name="new" value={$row[1]} type="radio"
           sigil="new-radio"
           checked={$new == $row[1]} />;
         if ($new == $row[1]) {
           $xnew = 'new-now';
         }
       } else {
         $rradio = null;
       }
 
       if ($row[3]) {
         $unit_star = 'star-none';
         switch ($row[6]) {
           case Diff::UNIT_FAIL:
           case Diff::UNIT_WARN: $unit_star = 'star-warn'; break;
           case Diff::UNIT_OKAY: $unit_star = 'star-okay'; break;
         }
 
         $lint_star = 'star-none';
         switch ($row[7]) {
           case Diff::LINT_FAIL:
           case Diff::LINT_WARNINGS: $lint_star = 'star-warn'; break;
           case Diff::LINT_OKAY:     $lint_star = 'star-okay'; break;
         }
 
         $star = "\xE2\x98\x85";
 
         $unit_star =
           <span class={$unit_star}>
             <span class="star">{$star}</span>
           </span>;
 
         $lint_star =
           <span class={$lint_star}>
             <span class="star">{$star}</span>
           </span>;
       } else {
         $unit_star = null;
         $lint_star = null;
       }
 
       $diff_table->appendChild(
         <tr class={++$ii % 2 ? 'alt' : null}>
           <td class="name">{$row[0]}</td>
           <td class="diffid">{$row[1]}</td>
           <td class="desc">{$row[5]}</td>
           <td class="age">{$row[4] ? ago(time() - $row[4]) : null}</td>
           <td class="star">{$lint_star}</td>
           <td class="star">{$unit_star}</td>
           <td class={"old {$xold}"}>{$lradio}</td>
           <td class={"new {$xnew}"}>{$rradio}</td>
         </tr>);
     }
 
     Javelin::initBehavior('differential-diff-radios', array(
       'radios' => $old_ids,
     ));
 
     $diff_table->appendChild(
       <tr>
         <td colspan="8" class="diff-differ-submit">
           <label>Whitespace Changes:</label>
           {id(<select name="whitespace" />)->setOptions(
             array(
               'ignore-all'      => 'Ignore All',
               'ignore-trailing' => 'Ignore Trailing',
               'show-all'        => 'Show All',
             ), $request->getStr('whitespace'))}{' '}
           <button type="submit">Show Diff</button>
         </td>
       </tr>);
 
     $diff_table =
       <div class="differential-table-of-contents">
         <h1>Revision Update History</h1>
         <form action={URI::getRequestURI()} method="get">
           {$diff_table}
         </form>
       </div>;
 
 
     $load_ids = array_filter(array($old, $diff->getID()));
 
     $viewer_id = $this->getRequest()->getViewerContext()->getUserID();
 
     $raw_objects = queryfx_all(
       smc_get_db('cdb.differential', 'r'),
       'SELECT * FROM changeset WHERE changeset.diffID IN (%Ld)',
       $load_ids);
 
     $raw_objects = array_group($raw_objects, 'diffID');
     $objects = $raw_objects[$diff->getID()];
 
     if (!$objects) {
       $changesets = array();
     } else {
       $changesets = id(new DifferentialChangeset())->loadAllFromArray($objects);
     }
 
     $against_warn = null;
     $against_map = array();
     $visible_changesets = array();
     if ($old) {
       $old_diff = $diffs[$old];
       $new_diff = $diff;
       $old_path = $old_diff->getSourcePath();
       $new_path = $new_diff->getSourcePath();
 
       $old_prefix = null;
       $new_prefix = null;
       if ((strlen($old_path) < strlen($new_path)) &&
           (!strncmp($old_path, $new_path, strlen($old_path)))) {
         $old_prefix = substr($new_path, strlen($old_path));
       }
       if ((strlen($new_path) < strlen($old_path)) &&
           (!strncmp($old_path, $new_path, strlen($new_path)))) {
         $new_prefix = substr($old_path, strlen($new_path));
       }
 
       $old_changesets = id(new DifferentialChangeset())
         ->loadAllFromArray($raw_objects[$old]);
       $old_changesets = array_pull($old_changesets, null, 'getFilename');
       if ($new_prefix) {
         $rekeyed_map = array();
         foreach ($old_changesets as $key => $value) {
           $rekeyed_map[$new_prefix.$key] = $value;
         }
         $old_changesets = $rekeyed_map;
       }
 
       foreach ($changesets as $key => $changeset) {
         $file = $old_prefix.$changeset->getFilename();
         if (isset($old_changesets[$file])) {
           $checksum = $changeset->getChecksum();
           if ($checksum !== null &&
               $checksum == $old_changesets[$file]->getChecksum()) {
             unset($changesets[$key]);
             unset($old_changesets[$file]);
           } else {
             $against_map[$changeset->getID()] = $old_changesets[$file]->getID();
             unset($old_changesets[$file]);
           }
         }
       }
 
       foreach ($old_changesets as $changeset) {
         $changesets[$changeset->getID()] = $changeset;
         $against_map[$changeset->getID()] = -1;
       }
 
       $against_warn =
         <tools:notice title="NOTE - Diff of Diffs">
           You are viewing a synthetic diff between two previous diffs in this
           revision. You can not add new inline comments (for now).
         </tools:notice>;
     } else {
       $visible_changesets = array_pull($changesets, 'getID');
     }
 
     $changesets = array_psort($changesets, 'getSortKey');
     $all_changesets = $changesets;
 
     $warning = null;
     $limit = 100;
     if (count($changesets) > $limit && !$this->getRequest()->getStr('large')) {
       $count = number_format(count($changesets));
       $warning =
         <tools:notice title="Very Large Diff">
           This diff is extremely large and affects {$count} files. Only the
           first {number_format($limit)} files are shown.
           <strong>
             <a href={$revision->getURI().'?large=true'}>Show All Files</a>
           </strong>
         </tools:notice>;
       $changesets = array_slice($changesets, 0, $limit);
       if (!$old) {
         $visible_changesets = array_pull($changesets, 'getID');
       }
     }
 
     $detail_view =
       <differential:changeset-detail-view
         changesets={$changesets}
           revision={$revision}
            against={$against_map}
               edit={empty($against_map)}
         whitespace={$request->getStr('whitespace')} />;
 
     $table_of_contents =
       <differential:changeset-table-of-contents
         changesets={$all_changesets} />;
 
     $implied_feedback = array();
     foreach (array(
       'summarize'   => $revision->getSummary(),
       'testplan'    => $revision->getTestPlan(),
       'annotate'    => $revision->getNotes(),
     ) as $type => $text) {
       if (!strlen($text)) {
         continue;
       }
       $implied_feedback[] = id(new DifferentialFeedback())
         ->setUserID($revision->getOwnerID())
         ->setAction($type)
         ->setDateCreated($revision->getDateCreated())
         ->setContent($text);
     }
 
     $feedback = id(new DifferentialFeedback())->loadAllWithRevision($revision);
     $feedback = array_merge($implied_feedback, $feedback);
 
     $inline_comments = $this->loadInlineComments($feedback, $changesets);
 
     $diff_map = array();
     $diffs = array_psort($diffs, 'getID');
     foreach ($diffs as $diff) {
       $diff_map[$diff->getID()] = count($diff_map) + 1;
     }
     $visible_changesets = array_fill_keys($visible_changesets, true);
     $hidden_changesets = array();
     foreach ($changesets as $changeset) {
       $id = $changeset->getID();
       if (isset($visible_changesets[$id])) {
         continue;
       }
       $hidden_changesets[$id] = $diff_map[$changeset->getDiffID()];
     }
 
     $revision->loadRelationships();
     $ccs = $revision->getCCFBIDs();
     $reviewers = $revision->getReviewers();
 
     $actors = array_pull($feedback, 'getUserID');
     $actors[] = $revision->getOwnerID();
 
     $tasks = array();
     assoc_get_by_type(
       $revision->getFBID(),
       22284182462, // TODO: include issue, DIFFCAMP_TASK_ASSOC
       $start = null,
       $limit = null,
       $pending = true,
       $tasks);
     memcache_dispatch();
     $tasks = array_keys($tasks);
 
     $preparer = new Preparer();
       $fbids = array_merge_fast(
         array($actors, array($viewer_id), $reviewers, $ccs, $tasks),
         true);
       $handles = array();
       $handle_data = id(new ToolsHandleData($fbids, $handles))
         ->needNames()
         ->needAlternateNames()
         ->needAlternateIDs()
         ->needThumbnails();
       $preparer->waitFor($handle_data);
     $preparer->go();
 
     $revision->attachTaskHandles(array_select_keys($handles, $tasks));
 
     $inline_comments = array_group($inline_comments, 'getFeedbackID');
 
     $engine = new RemarkupEngine();
     $engine->enableFeature(RemarkupEngine::FEATURE_GUESS_IMAGES);
     $engine->enableFeature(RemarkupEngine::FEATURE_YOUTUBE);
     $engine->setCurrentSandcastle($this->getSandcastleURI($target_diff));
     $feed = array();
     foreach ($feedback as $comment) {
       $inlines = null;
       if (isset($inline_comments[$comment->getID()])) {
         $inlines = $inline_comments[$comment->getID()];
       }
       $feed[] =
         <differential:feedback
             feedback={$comment}
               handle={$handles[$comment->getUserID()]}
               engine={$engine}
               inline={$inlines}
           changesets={$changesets}
               hidden={$hidden_changesets} />;
     }
 
     $feed = $this->renderFeedbackList($feed, $feedback, $viewer_id);
 
     $fields = $this->getDetailFields($revision, $diff, $handles);
     $table = <table class="differential-revision-properties" />;
     foreach ($fields as $key => $value) {
       $table->appendChild(
         <tr>
           <th>{$key}:</th><td>{$value}</td>
         </tr>);
     }
 
     $quick_links = $this->getQuickLinks($revision);
 
     $edit_link = null;
     if ($revision->getOwnerID() == $viewer_id) {
       $edit_link = '/differential/revision/edit/'.$revision->getID().'/';
       $edit_link =
         <x:frag>
           {' '}(<a href={$edit_link}>Edit Revision</a>)
         </x:frag>;
     }
 
     $info =
       <div class="differential-revision-information">
         <div class="differential-revision-actions">
           {$quick_links}
         </div>
         <div class="differential-revision-detail">
           <h1>{$revision->getName()}{$edit_link}</h1>
           {$table}
         </div>
       </div>;
 
     $actions = $this->getRevisionActions($revision);
     $revision_id = $revision->getID();
 
     Javelin::initBehavior(
       'differential-feedback-preview',
       array(
         'uri'     => '/differential/preview/'.$revision->getFBID().'/',
         'preview' => 'overall-feedback-preview',
         'action'  => 'feedback-action',
         'content' => 'feedback-content',
       ));
 
     Javelin::initBehavior(
       'differential-inline-comment-preview',
       array(
         'uri' => '/differential/inline-preview/'.$revision_id.'/'.$new.'/',
         'preview' => 'inline-comment-preview',
       ));
 
     $content = SavedCopy::loadData(
       $viewer_id,
       SavedCopy::Type_DifferentialRevisionFeedback,
       $revision->getFBID());
 
 
     $inline_comment_container =
         <div id="inline-comment-preview"><p>Loading...</p></div>;
 
     $feedback = id(new DifferentialFeedback())
       ->setAction('none')
       ->setUserID($viewer_id)
       ->setContent($content);
 
     $preview =
       <div class="differential-feedback differential-feedback-preview">
         <div id="overall-feedback-preview">
           <differential:feedback
             feedback={$feedback}
               engine={$engine}
              preview={true}
               handle={$handles[$viewer_id]} />
         </div>
         {$inline_comment_container}
       </div>;
 
     $syntax_link =
       <a href={'http://www.intern.facebook.com/intern/wiki/index.php' .
                '/Articles/Remarkup_Syntax_Reference'}
          target="_blank"
          tabindex="4">Remarkup Reference</a>;
 
     Javelin::initBehavior(
       'differential-add-reviewers',
       array(
         'src'       => redirect_str('/datasource/employee/', 'tools'),
         'tokenizer' => 'reviewer-tokenizer',
         'select'    => 'feedback-action',
         'row'       => 'reviewer-tokenizer-row',
       ));
 
     $feedback_form =
       <x:frag>
         <div class="differential-feedback-form">
           <tools:form
             method="post"
             action={"/differential/revision/feedback/{$revision_id}/"}>
             <h1>Provide Feedback</h1>
             <tools:fieldset>
               <tools:control type="select" label="Action">
                 {id(<select name="action" id="feedback-action"
                       tabindex="1" />)
                   ->setOptions($actions)}
               </tools:control>
               <tools:control type="text" label="Reviewers"
                 style="display: none;"
                 id="reviewer-tokenizer-row">
                 <javelin:tokenizer-template
                   id="reviewer-tokenizer"
                   name="reviewers" />
               </tools:control>
               <tools:control type="textarea" label="Feedback"
                 caption={$syntax_link}>
                 <tools:droppable-textarea id="feedback-content" name="feedback"
                   tabindex="2">
                   {$content}
                 </tools:droppable-textarea>
               </tools:control>
               <tools:control type="submit">
                 <button type="submit"
                   tabindex="3">Clowncopterize</button>
               </tools:control>
             </tools:fieldset>
           </tools:form>
         </div>
         {$preview}
       </x:frag>;
 
     $notice = null;
     if ($this->getRequest()->getBool('diff_changed')) {
       $notice =
         <tools:notice title="Revision Updated Recently">
           This revision was updated with a <strong>new diff</strong> while you
           were providing feedback. Your inline comments appear on the
           <strong>old diff</strong>.
         </tools:notice>;
     }
 
     return
       <differential:standard-page title={$revision->getName()}>
         <div class="differential-primary-pane">
           {$warning}
           {$notice}
           {$info}
           <div class="differential-feedback">
             {$feed}
           </div>
           {$diff_table}
           {$table_of_contents}
           {$against_warn}
           {$detail_view}
           {$feedback_form}
         </div>
       </differential:standard-page>;
   }
 
   protected function getQuickLinks(DifferentialRevision $revision) {
 
     $viewer_id = $this->getRequest()->getViewerContext()->getUserID();
     $viewer_is_owner = ($viewer_id == $revision->getOwnerID());
     $viewer_is_reviewer =
       ((array_search($viewer_id, $revision->getReviewers())) !== false);
     $viewer_is_cc =
       ((array_search($viewer_id, $revision->getCCFBIDs())) !== false);
     $status = $revision->getStatus();
 
     $links = array();
 
     if (!$viewer_is_owner && !$viewer_is_reviewer) {
       $action = $viewer_is_cc
         ? 'rem'
         : 'add';
       $revision_id = $revision->getID();
       $href = "/differential/subscribe/{$action}/{$revision_id}";
       $links[] = array(
         $viewer_is_cc ? 'subscribe-disabled' : 'subscribe-enabled',
         <a href={$href}>{$viewer_is_cc ? 'Unsubscribe' : 'Subscribe'}</a>,
       );
     } else {
       $links[] = array(
         'subscribe-disabled unavailable',
         <a>Automatically Subscribed</a>,
       );
     }
 
     $blast_uri = RedirectURI(
       '/intern/differential/?action=blast&fbid='.$revision->getFBID())
       ->setTier('intern');
     $links[] = array(
       'blast',
       <a href={$blast_uri}>Blast Revision</a>,
     );
 
     $blast_uri = RedirectURI(
       '/intern/differential/?action=tasks&fbid='.$revision->getFBID())
       ->setTier('intern');
     $links[] = array(
       'tasks',
       <a href={$blast_uri}>Edit Tasks</a>,
     );
 
     if ($viewer_is_owner && false) {
       $perflab_uri = RedirectURI(
         '/intern/differential/?action=perflab&fbid='.$revision->getFBID())
         ->setTier('intern');
       $links[] = array(
         'perflab',
         <a href={$perflab_uri}>Run in Perflab</a>,
       );
     }
 
     $engineering_repository_id = RepositoryRef::getByCallsign('E')->getID();
     $svn_revision = $revision->getSVNRevision();
     if ($status == DifferentialConstants::COMMITTED &&
         $svn_revision &&
         $revision->getRepositoryID() == $engineering_repository_id) {
       $href = '/intern/push/request.php?rev='.$svn_revision;
       $href = RedirectURI($href)->setTier('intern');
       $links[] = array(
         'merge',
         <a href={$href} id="ask_for_merge_link">Ask for Merge</a>,
       );
     }
 
     $links[] = array(
       'herald-transcript',
       <a href={"/herald/transcript/?fbid=".$revision->getFBID()}
         >Herald Transcripts</a>,
     );
     $links[] = array(
       'metamta-transcript',
       <a href={"/mail/?view=all&fbid=".$revision->getFBID()}
         >MetaMTA Transcripts</a>,
     );
 
 
     $list = <ul class="differential-actions" />;
     foreach ($links as $link) {
       list($class, $tag) = $link;
       $list->appendChild(<li class={$class}>{$tag}</li>);
     }
 
     return $list;
   }
 
   protected function getDetailFields(
     DifferentialRevision $revision,
     Diff $diff,
     array $handles) {
 
     $fields = array();
     $fields['Revision Status'] = $this->getRevisionStatusDisplay($revision);
 
     $author = $revision->getOwnerID();
     $fields['Author'] = <tools:handle handle={$handles[$author]}
                                         link={true} />;
 
     $sandcastle = $this->getSandcastleURI($diff);
     if ($sandcastle) {
       $fields['Sandcastle'] = <a href={$sandcastle}>{$sandcastle}</a>;
     }
 
     $path = $diff->getSourcePath();
     if ($path) {
       $host = $diff->getSourceMachine();
       $branch = $diff->getGitBranch() ? ' (' . $diff->getGitBranch() . ')' : '';
 
       if ($host) {
         $user = $handles[$this->getRequest()->getViewerContext()->getUserID()]
           ->getName();
         $fields['Path'] =
           <x:frag>
             <a href={"ssh://{$user}@{$host}"}>{$host}</a>:{$path}{$branch}
           </x:frag>;
       } else {
         $fields['Path'] = $path;
       }
     }
 
     $reviewer_links = array();
     foreach ($revision->getReviewers() as $reviewer) {
       $reviewer_links[] = <tools:handle handle={$handles[$reviewer]}
                                           link={true} />;
     }
     if ($reviewer_links) {
       $fields['Reviewers'] = array_implode(', ', $reviewer_links);
     } else {
       $fields['Reviewers'] = <em>None</em>;
     }
 
     $ccs = $revision->getCCFBIDs();
     if ($ccs) {
       $links = array();
       foreach ($ccs as $cc) {
         $links[] = <tools:handle handle={$handles[$cc]}
                                    link={true} />;
       }
       $fields['CCs'] = array_implode(', ', $links);
     }
 
     $blame_rev = $revision->getSvnBlameRevision();
     if ($blame_rev) {
       if ($revision->getRepositoryRef() && is_numeric($blame_rev)) {
         $ref = new RevisionRef($revision->getRepositoryRef(), $blame_rev);
         $fields['Blame Revision'] =
           <a href={URI($ref->getDetailURL())}>
             {$ref->getName()}
           </a>;
       } else {
         $fields['Blame Revision'] = $blame_rev;
       }
     }
 
     $tasks = $revision->getTaskHandles();
 
     if ($tasks) {
       $links = array();
       foreach ($tasks as $task) {
         $links[] = <tools:handle handle={$task} link={true} />;
       }
       $fields['Tasks'] = array_implode(<br />, $links);
     }
 
     $bugzilla_id = $revision->getBugzillaID();
     if ($bugzilla_id) {
       $href = 'http://bugs.developers.facebook.com/show_bug.cgi?id='.
         $bugzilla_id;
       $fields['Bugzilla'] = <a href={$href}>{'#'.$bugzilla_id}</a>;
     }
 
     $fields['Apply Patch'] = <tt>arc patch --revision {$revision->getID()}</tt>;
 
     if ($diff->getParentRevisionID()) {
       $parent = id(new DifferentialRevision())->load(
         $diff->getParentRevisionID());
       if ($parent) {
         $fields['Depends On'] =
           <a href={$parent->getURI()}>
             D{$parent->getID()}: {$parent->getName()}
           </a>;
       }
     }
 
     $star = <span class="star">{"\xE2\x98\x85"}</span>;
 
     Javelin::initBehavior('differential-star-more');
 
     switch ($diff->getLinted()) {
       case Diff::LINT_FAIL:
         $more = $this->renderDiffPropertyMoreLink($diff, 'lint');
         $fields['Lint'] =
           <x:frag>
             <span class="star-warn">{$star} Lint Failures</span>
             {$more}
           </x:frag>;
         break;
       case Diff::LINT_WARNINGS:
         $more = $this->renderDiffPropertyMoreLink($diff, 'lint');
         $fields['Lint'] =
           <x:frag>
             <span class="star-warn">{$star} Lint Warnings</span>
             {$more}
           </x:frag>;
         break;
       case Diff::LINT_OKAY:
         $fields['Lint'] =
           <span class="star-okay">{$star} Lint Free</span>;
         break;
       default:
       case Diff::LINT_NO:
         $fields['Lint'] =
           <span class="star-none">{$star} Not Linted</span>;
         break;
     }
 
     $unit_details = false;
     switch ($diff->getUnitTested()) {
       case Diff::UNIT_FAIL:
         $fields['Unit Tests'] =
             <span class="star-warn">{$star} Unit Test Failures</span>;
         $unit_details = true;
         break;
       case Diff::UNIT_WARN:
         $fields['Unit Tests'] =
             <span class="star-warn">{$star} Unit Test Warnings</span>;
         $unit_details = true;
         break;
       case Diff::UNIT_OKAY:
         $fields['Unit Tests'] =
           <span class="star-okay">{$star} Unit Tests Passed</span>;
         $unit_details = true;
         break;
       case Diff::UNIT_NO_TESTS:
         $fields['Unit Tests'] =
           <span class="star-none">{$star} No Test Coverage</span>;
         break;
       case Diff::UNIT_NO:
       default:
         $fields['Unit Tests'] =
           <span class="star-none">{$star} Not Unit Tested</span>;
         break;
     }
 
     if ($unit_details) {
       $fields['Unit Tests'] =
         <x:frag>
           {$fields['Unit Tests']}
           {$this->renderDiffPropertyMoreLink($diff, 'unit')}
         </x:frag>;
     }
 
     $platform_impact = $revision->getPlatformImpact();
     if ($platform_impact) {
       $fields['Platform Impact'] =
         <text linebreaks="true">{$platform_impact}</text>;
     }
 
     return $fields;
   }
 
   protected function renderDiffPropertyMoreLink(Diff $diff, $name) {
     $target = <div class="star-more"
                    style="display: none;">
                 <div class="star-loading">Loading...</div>
               </div>;
     $meta = array(
       'target'  => $target->requireUniqueID(),
       'uri'     => '/differential/diffprop/'.$diff->getID().'/'.$name.'/',
     );
     $more =
       <span sigil="star-link-container">
         &middot;
         <a mustcapture="true"
                  sigil="star-more"
                   href="#"
                   meta={$meta}>Show Details</a>
       </span>;
     return <x:frag>{$more}{$target}</x:frag>;
   }
 
 
 
   protected function loadInlineComments(array $feedback, array &$changesets) {
 
     $inline_comments = array();
     $feedback_ids = array_filter(array_pull($feedback, 'getID'));
     if (!$feedback_ids) {
       return $inline_comments;
     }
 
     $inline_comments = id(new DifferentialInlineComment())
       ->loadAllWhere('feedbackID in (%Ld)', $feedback_ids);
 
     $load_changesets = array();
     $load_hunks = array();
     foreach ($inline_comments as $inline) {
       $changeset_id = $inline->getChangesetID();
       if (isset($changesets[$changeset_id])) {
         continue;
       }
       $load_changesets[$changeset_id] = true;
     }
 
     $more_changesets = array();
     if ($load_changesets) {
       $changeset_ids = array_keys($load_changesets);
       $more_changesets += id(new DifferentialChangeset())
         ->loadAllWithIDs($changeset_ids);
     }
 
     if ($more_changesets) {
       $changesets += $more_changesets;
       $changesets = array_psort($changesets, 'getSortKey');
     }
 
     return $inline_comments;
   }
 
 
 
   protected function getRevisionStatusDisplay(DifferentialRevision $revision) {
     $viewer_id = $this->getRequest()->getViewerContext()->getUserID();
     $viewer_is_owner = ($viewer_id == $revision->getOwnerID());
     $status = $revision->getStatus();
 
     $more = null;
     switch ($status) {
       case DifferentialConstants::NEEDS_REVIEW:
         $message = 'Pending Review';
         break;
       case DifferentialConstants::NEEDS_REVISION:
         $message = 'Awaiting Revision';
         if ($viewer_is_owner) {
           $more = 'Make the requested changes and update the revision.';
         }
         break;
       case DifferentialConstants::ACCEPTED:
         $message = 'Ready for Commit';
         if ($viewer_is_owner) {
           $more =
             <x:frag>
               Run <tt>arc commit</tt> (svn) or <tt>arc amend</tt> (git) to
               proceed.
             </x:frag>;
         }
         break;
       case DifferentialConstants::COMMITTED:
         $message = 'Committed';
         $ref = $revision->getRevisionRef();
         $more = $ref
                 ? (<a href={URI($ref->getDetailURL())}>
                      {$ref->getName()}
                    </a>)
                 : null;
 
         $engineering_repository_id = RepositoryRef::getByCallsign('E')->getID();
         if ($revision->getSVNRevision() &&
             $revision->getRepositoryID() == $engineering_repository_id) {
           Javelin::initBehavior(
             'differential-revtracker-status',
             array(
               'uri' => '/differential/revtracker/'.$revision->getID().'/',
               'statusId' => 'revtracker_status',
               'mergeLinkId' => 'ask_for_merge_link',
             ));
         }
         break;
       case DifferentialConstants::ABANDONED:
         $message = 'Abandoned';
         break;
       default:
         throw new Exception("Unknown revision status.");
     }
 
     if ($more) {
       $message =
         <x:frag>
           <strong id="revtracker_status">{$message}</strong>
           &middot; {$more}
         </x:frag>;
     } else {
       $message = <strong id="revtracker_status">{$message}</strong>;
     }
 
     return $message;
   }
 
   protected function renderFeedbackList(array $xhp, array $obj, $viewer_id) {
 
     // Use magical heuristics to try to hide older comments.
 
     $obj = array_reverse($obj);
     $obj = array_values($obj);
     $xhp = array_reverse($xhp);
     $xhp = array_values($xhp);
 
     $last_comment = null;
     foreach ($obj as $position => $feedback) {
       if ($feedback->getUserID() == $viewer_id) {
         if ($last_comment === null) {
           $last_comment = $position;
         } else if ($last_comment == $position - 1) {
           // If you made consecuitive comments, show them all. This is a spaz
           // rule for epriestley comments.
           $last_comment = $position;
         }
       }
     }
 
     $header = array();
 
     $hide = array();
     if ($last_comment !== null) {
       foreach ($obj as $position => $feedback) {
         $action = $feedback->getAction();
         if ($action == 'testplan' || $action == 'summarize') {
           // Always show summary and test plan.
           $header[] = $xhp[$position];
           unset($xhp[$position]);
           continue;
         }
 
         if ($position <= $last_comment) {
           // Always show comments after your last comment.
           continue;
         }
 
         if ($position < 3) {
           // Always show the most recent 3 comments.
           continue;
         }
 
         // Hide everything else.
         $hide[] = $position;
       }
     }
 
     if (count($hide) <= 3) {
       // Don't hide if there's not much to hide.
       $hide = array();
     }
 
     $header = array_reverse($header);
 
     $hidden = array_select_keys($xhp, $hide);
     $visible = array_diff_key($xhp, $hidden);
 
     $visible = array_reverse($visible);
     $hidden  = array_reverse($hidden);
 
     if ($hidden) {
       Javelin::initBehavior(
         'differential-show-all-feedback',
         array(
           'markup' => id(<x:frag>{$hidden}</x:frag>)->toString(),
         ));
       $hidden =
         <div sigil="all-feedback-container">
           <div class="older-replies-are-hidden">
             {number_format(count($hidden))} older replies are hidden.
             <a href="#" sigil="show-all-feedback"
               mustcapture="true">Show all feedback.</a>
           </div>
         </div>;
     } else {
       $hidden = null;
     }
 
     return
       <x:frag>
         {$header}
         {$hidden}
         {$visible}
       </x:frag>;
   }
 
 }
   protected function getDetailFields(
     DifferentialRevision $revision,
     Diff $diff,
     array $handles) {
 
     $fields = array();
     $fields['Revision Status'] = $this->getRevisionStatusDisplay($revision);
 
     $author = $revision->getOwnerID();
     $fields['Author'] = <tools:handle handle={$handles[$author]}
                                         link={true} />;
 
     $sandcastle = $this->getSandcastleURI($diff);
     if ($sandcastle) {
       $fields['Sandcastle'] = <a href={$sandcastle}>{$sandcastle}</a>;
     }
 
     $path = $diff->getSourcePath();
     if ($path) {
       $host = $diff->getSourceMachine();
       $branch = $diff->getGitBranch() ? ' (' . $diff->getGitBranch() . ')' : '';
 
       if ($host) {
         $user = $handles[$this->getRequest()->getViewerContext()->getUserID()]
           ->getName();
         $fields['Path'] =
           <x:frag>
             <a href={"ssh://{$user}@{$host}"}>{$host}</a>:{$path}{$branch}
           </x:frag>;
       } else {
         $fields['Path'] = $path;
       }
     }
 
     $reviewer_links = array();
     foreach ($revision->getReviewers() as $reviewer) {
       $reviewer_links[] = <tools:handle handle={$handles[$reviewer]}
                                           link={true} />;
     }
     if ($reviewer_links) {
       $fields['Reviewers'] = array_implode(', ', $reviewer_links);
     } else {
       $fields['Reviewers'] = <em>None</em>;
     }
 
     $ccs = $revision->getCCFBIDs();
     if ($ccs) {
       $links = array();
       foreach ($ccs as $cc) {
         $links[] = <tools:handle handle={$handles[$cc]}
                                    link={true} />;
       }
       $fields['CCs'] = array_implode(', ', $links);
     }
 
     $blame_rev = $revision->getSvnBlameRevision();
     if ($blame_rev) {
       if ($revision->getRepositoryRef() && is_numeric($blame_rev)) {
         $ref = new RevisionRef($revision->getRepositoryRef(), $blame_rev);
         $fields['Blame Revision'] =
           <a href={URI($ref->getDetailURL())}>
             {$ref->getName()}
           </a>;
       } else {
         $fields['Blame Revision'] = $blame_rev;
       }
     }
 
     $tasks = $revision->getTaskHandles();
 
     if ($tasks) {
       $links = array();
       foreach ($tasks as $task) {
         $links[] = <tools:handle handle={$task} link={true} />;
       }
       $fields['Tasks'] = array_implode(<br />, $links);
     }
 
     $bugzilla_id = $revision->getBugzillaID();
     if ($bugzilla_id) {
       $href = 'http://bugs.developers.facebook.com/show_bug.cgi?id='.
         $bugzilla_id;
       $fields['Bugzilla'] = <a href={$href}>{'#'.$bugzilla_id}</a>;
     }
 
     $fields['Apply Patch'] = <tt>arc patch --revision {$revision->getID()}</tt>;
 
     if ($diff->getParentRevisionID()) {
       $parent = id(new DifferentialRevision())->load(
         $diff->getParentRevisionID());
       if ($parent) {
         $fields['Depends On'] =
           <a href={$parent->getURI()}>
             D{$parent->getID()}: {$parent->getName()}
           </a>;
       }
     }
 
     $star = <span class="star">{"\xE2\x98\x85"}</span>;
 
     Javelin::initBehavior('differential-star-more');
 
     switch ($diff->getLinted()) {
       case Diff::LINT_FAIL:
         $more = $this->renderDiffPropertyMoreLink($diff, 'lint');
         $fields['Lint'] =
           <x:frag>
             <span class="star-warn">{$star} Lint Failures</span>
             {$more}
           </x:frag>;
         break;
       case Diff::LINT_WARNINGS:
         $more = $this->renderDiffPropertyMoreLink($diff, 'lint');
         $fields['Lint'] =
           <x:frag>
             <span class="star-warn">{$star} Lint Warnings</span>
             {$more}
           </x:frag>;
         break;
       case Diff::LINT_OKAY:
         $fields['Lint'] =
           <span class="star-okay">{$star} Lint Free</span>;
         break;
       default:
       case Diff::LINT_NO:
         $fields['Lint'] =
           <span class="star-none">{$star} Not Linted</span>;
         break;
     }
 
     $unit_details = false;
     switch ($diff->getUnitTested()) {
       case Diff::UNIT_FAIL:
         $fields['Unit Tests'] =
             <span class="star-warn">{$star} Unit Test Failures</span>;
         $unit_details = true;
         break;
       case Diff::UNIT_WARN:
         $fields['Unit Tests'] =
             <span class="star-warn">{$star} Unit Test Warnings</span>;
         $unit_details = true;
         break;
       case Diff::UNIT_OKAY:
         $fields['Unit Tests'] =
           <span class="star-okay">{$star} Unit Tests Passed</span>;
         $unit_details = true;
         break;
       case Diff::UNIT_NO_TESTS:
         $fields['Unit Tests'] =
           <span class="star-none">{$star} No Test Coverage</span>;
         break;
       case Diff::UNIT_NO:
       default:
         $fields['Unit Tests'] =
           <span class="star-none">{$star} Not Unit Tested</span>;
         break;
     }
 
     if ($unit_details) {
       $fields['Unit Tests'] =
         <x:frag>
           {$fields['Unit Tests']}
           {$this->renderDiffPropertyMoreLink($diff, 'unit')}
         </x:frag>;
     }
 
     $platform_impact = $revision->getPlatformImpact();
     if ($platform_impact) {
       $fields['Platform Impact'] =
         <text linebreaks="true">{$platform_impact}</text>;
     }
 
     return $fields;
   }
 
 
 */
diff --git a/src/applications/differential/editor/comment/DifferentialCommentEditor.php b/src/applications/differential/editor/comment/DifferentialCommentEditor.php
new file mode 100755
index 000000000..b9e23528a
--- /dev/null
+++ b/src/applications/differential/editor/comment/DifferentialCommentEditor.php
@@ -0,0 +1,329 @@
+<?php
+
+/*
+ * Copyright 2011 Facebook, Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+class DifferentialCommentEditor {
+
+  protected $revision;
+  protected $actorPHID;
+  protected $action;
+
+  protected $attachInlineComments;
+  protected $message;
+  protected $addCC;
+  protected $changedByCommit;
+  protected $addedReviewers = array();
+
+  public function __construct(
+    DifferentialRevision $revision,
+    $actor_phid,
+    $action) {
+
+    $this->revision = $revision;
+    $this->actorPHID  = $actor_phid;
+    $this->action   = $action;
+  }
+
+  public function setMessage($message) {
+    $this->message = $message;
+    return $this;
+  }
+
+  public function setAttachInlineComments($attach) {
+    $this->attachInlineComments = $attach;
+    return $this;
+  }
+
+  public function setAddCC($add) {
+    $this->addCC = $add;
+    return $this;
+  }
+
+  public function setChangedByCommit($changed_by_commit) {
+    $this->changedByCommit = $changed_by_commit;
+    return $this;
+  }
+
+  public function getChangedByCommit() {
+    return $this->changedByCommit;
+  }
+
+  public function setAddedReviewers($added_reviewers) {
+    $this->addedReviewers = $added_reviewers;
+    return $this;
+  }
+
+  public function getAddedReviewers() {
+    return $this->addedReviewers;
+  }
+
+  public function save() {
+    $revision = $this->revision;
+    $action = $this->action;
+    $actor_phid = $this->actorPHID;
+    $actor_is_author = ($actor_phid == $revision->getAuthorPHID());
+    $revision_status = $revision->getStatus();
+
+    $revision->loadRelationships();
+    $reviewer_phids = $revision->getReviewers();
+    if ($reviewer_phids) {
+      $reviewer_phids = array_combine($reviewer_phids, $reviewer_phids);
+    }
+
+    switch ($action) {
+      case DifferentialAction::ACTION_COMMENT:
+        break;
+
+      case DifferentialAction::ACTION_RESIGN:
+        if ($actor_is_author) {
+          throw new Exception('You can not resign from your own revision!');
+        }
+        if (isset($reviewer_phids[$actor_phid])) {
+          DifferentialRevisionEditor::alterReviewers(
+            $revision,
+            $reviewer_phids,
+            $rem = array($actor_phid),
+            $add = array(),
+            $actor_phid);
+        }
+        break;
+
+      case DifferentialAction::ACTION_ABANDON:
+        if (!$actor_is_author) {
+          throw new Exception('You can only abandon your revisions.');
+        }
+        if ($revision_status == DifferentialRevisionStatus::COMMITTED) {
+          throw new Exception('You can not abandon a committed revision.');
+        }
+        if ($revision_status == DifferentialRevisionStatus::ABANDONED) {
+          $action = DifferentialAction::ACTION_COMMENT;
+          break;
+        }
+
+        $revision
+          ->setStatus(DifferentialRevisionStatus::ABANDONED)
+          ->save();
+        break;
+
+      case DifferentialAction::ACTION_ACCEPT:
+        if ($actor_is_author) {
+          throw new Exception('You can not accept your own revision.');
+        }
+        if (($revision_status != DifferentialRevisionStatus::NEEDS_REVIEW) &&
+            ($revision_status != DifferentialRevisionStatus::NEEDS_REVISION)) {
+          $action = DifferentialAction::ACTION_COMMENT;
+          break;
+        }
+
+        $revision
+          ->setStatus(DifferentialRevisionStatus::ACCEPTED)
+          ->save();
+
+        if (!isset($reviewer_phids[$actor_phid])) {
+          DifferentialRevisionEditor::addReviewers(
+            $revision,
+            $reviewer_phids,
+            $rem = array(),
+            $add = array($actor_phid),
+            $actor_phid);
+        }
+        break;
+
+      case DifferentialAction::ACTION_REQUEST:
+        if (!$actor_is_author) {
+          throw new Exception('You must own a revision to request review.');
+        }
+        if (($revision_status != DifferentialRevisionStatus::NEEDS_REVISION) &&
+            ($revision_status != DifferentialRevisionStatus::ACCEPTED)) {
+          $action = DifferentialAction::ACTION_COMMENT;
+          break;
+        }
+
+        $revision
+          ->setStatus(DifferentialRevisionStatus::NEEDS_REVIEW)
+          ->save();
+        break;
+
+      case DifferentialAction::ACTION_REJECT:
+        if ($actor_is_author) {
+          throw new Exception(
+            'You can not request changes to your own revision.');
+        }
+        if (($revision_status != DifferentialRevisionStatus::NEEDS_REVIEW) &&
+            ($revision_status != DifferentialRevisionStatus::ACCEPTED)) {
+          $action = DifferentialAction::ACTION_COMMENT;
+          break;
+        }
+
+        if (!isset($reviewer_phids[$actor_phid])) {
+          DifferentialRevisionEditor::addReviewers(
+            $revision,
+            $reviewer_phids,
+            $rem = array(),
+            $add = array($actor_phid),
+            $actor_phid);
+        }
+
+        $revision
+          ->setStatus(DifferentialRevisionStatus::NEEDS_REVISION)
+          ->save();
+        break;
+
+      case DifferentialAction::ACTION_RECLAIM:
+        if (!$actor_is_author) {
+          throw new Exception('You can not reclaim a revision you do not own.');
+        }
+        if ($revision_status != DifferentialRevisionStatus::ABANDONED) {
+          $action = DifferentialAction::ACTION_COMMENT;
+          break;
+        }
+        $revision
+          ->setStatus(DifferentialRevisionStatus::NEEDS_REVIEW)
+          ->save();
+        break;
+
+      case DifferentialAction::ACTION_COMMIT:
+        // This is handled externally. (TODO)
+        break;
+
+      case DifferentialAction::ACTION_ADDREVIEWERS:
+        $added_reviewers = $this->getAddedReviewers();
+        foreach ($added_reviewers as $k => $user_phid) {
+          if ($user_phid == $revision->getAuthorPHID()) {
+            unset($added_reviewers[$k]);
+          }
+          if (!empty($reviewer_phids[$user_phid])) {
+            unset($added_reviewers[$k]);
+          }
+        }
+
+        $added_reviewers = array_unique($added_reviewers);
+
+        if ($added_reviewers) {
+          DifferentialRevisionEditor::addReviewers(
+            $revision,
+            $reviewer_phids,
+            $rem = array(),
+            $add = $added_reviewers,
+            $actor_phid);
+
+// TODO
+//          $unixnames = unixname_multi($added_reviewers);
+          $usernames = $added_reviewers;
+
+          $this->message =
+            'Added reviewers: '.implode(', ', $usernames)."\n\n".
+            $this->message;
+
+        } else {
+          $action = DifferentialAction::ACTION_COMMENT;
+        }
+        break;
+
+      default:
+        throw new Exception('Unsupported action.');
+    }
+
+    // Reload relationships to pick up any reviewer changes.
+    $revision->loadRelationships();
+
+/*
+  TODO
+
+    $inline_comments = array();
+    if ($this->attachInlineComments) {
+      $inline_comments = id(new DifferentialInlineComment())
+        ->loadAllUnsaved($revision, $this->actorPHID);
+    }
+*/
+
+    $comment = id(new DifferentialComment())
+      ->setAuthorPHID($this->actorPHID)
+      ->setRevisionID($revision->getID())
+      ->setAction($action)
+      ->setContent((string)$this->message)
+      ->save();
+
+/*
+    $diff = id(new Diff())->loadActiveWithRevision($revision);
+    $changesets = id(new DifferentialChangeset())->loadAllWithDiff($diff);
+
+    if ($inline_comments) {
+      // We may have feedback on non-current changesets. Rather than orphaning
+      // it, just submit it. This is non-ideal but not horrible.
+      $inline_changeset_ids = array_pull($inline_comments, 'getChangesetID');
+      $load = array();
+      foreach ($inline_changeset_ids as $id) {
+        if (empty($changesets[$id])) {
+          $load[] = $id;
+        }
+      }
+      if ($load) {
+        $changesets += id(new DifferentialChangeset())->loadAllWithIDs($load);
+      }
+      foreach ($inline_comments as $inline) {
+        $inline->setFeedbackID($feedback->getID());
+        $inline->save();
+      }
+    }
+*/
+
+    id(new DifferentialCommentMail(
+      $revision,
+      $this->actorPHID,
+      $comment,
+      /* $changesets TODO */ array(),
+      /* $inline_comments TODO */ array()))
+      ->setToPHIDs(
+        array_merge(
+          $revision->getReviewers(),
+          array($revision->getAuthorPHID())))
+      ->setCCPHIDs($revision->getCCPHIDs())
+      ->setChangedByCommit($this->getChangedByCommit())
+      ->send();
+
+/*
+
+  tODO
+
+    if ($this->addCC) {
+      require_module_lazy('site/tools/differential/lib/editor/revision');
+      DifferentialRevisionEditor::addCCFBID(
+        $revision,
+        $this->actorPHID,
+        $this->actorPHID);
+    }
+*/
+
+/*
+
+  TODO
+
+    $event = array(
+      'revision_id' => $revision->getID(),
+      'fbid'        => $revision->getFBID(),
+      'feedback_id' => $feedback->getID(),
+      'action'      => $feedback->getAction(),
+      'actor'       => $this->actorPHID,
+    );
+    id(new ToolsTimelineEvent('difx', fb_json_encode($event)))->record();
+*/
+
+    return $comment;
+  }
+
+}
diff --git a/src/applications/differential/mail/feedback/__init__.php b/src/applications/differential/editor/comment/__init__.php
similarity index 55%
copy from src/applications/differential/mail/feedback/__init__.php
copy to src/applications/differential/editor/comment/__init__.php
index dd323e630..5eb988d3a 100644
--- a/src/applications/differential/mail/feedback/__init__.php
+++ b/src/applications/differential/editor/comment/__init__.php
@@ -1,14 +1,18 @@
 <?php
 /**
  * This file is automatically generated. Lint this module to rebuild it.
  * @generated
  */
 
 
 
 phutil_require_module('phabricator', 'applications/differential/constants/action');
 phutil_require_module('phabricator', 'applications/differential/constants/revisionstatus');
-phutil_require_module('phabricator', 'applications/differential/mail/base');
+phutil_require_module('phabricator', 'applications/differential/editor/revision');
+phutil_require_module('phabricator', 'applications/differential/mail/comment');
+phutil_require_module('phabricator', 'applications/differential/storage/comment');
 
+phutil_require_module('phutil', 'utils');
 
-phutil_require_source('DifferentialFeedbackMail.php');
+
+phutil_require_source('DifferentialCommentEditor.php');
diff --git a/src/applications/differential/mail/base/DifferentialMail.php b/src/applications/differential/mail/base/DifferentialMail.php
index 6b30df6fb..322049845 100755
--- a/src/applications/differential/mail/base/DifferentialMail.php
+++ b/src/applications/differential/mail/base/DifferentialMail.php
@@ -1,311 +1,311 @@
 <?php
 
 /*
  * Copyright 2011 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 abstract class DifferentialMail {
 
   const SUBJECT_PREFIX  = '[Differential]';
 
   protected $to = array();
   protected $cc = array();
 
   protected $actorName;
   protected $actorID;
 
   protected $revision;
-  protected $feedback;
+  protected $comment;
   protected $changesets;
   protected $inlineComments;
   protected $isFirstMailAboutRevision;
   protected $isFirstMailToRecipients;
   protected $heraldTranscriptURI;
   protected $heraldRulesHeader;
 
   public function getActorName() {
     return $this->actorName;
   }
 
   public function setActorName($actor_name) {
     $this->actorName = $actor_name;
     return $this;
   }
 
   abstract protected function renderSubject();
   abstract protected function renderBody();
 
   public function setXHeraldRulesHeader($header) {
     $this->heraldRulesHeader = $header;
     return $this;
   }
 
   public function send() {
     $to_phids = $this->getToPHIDs();
     if (!$to_phids) {
       throw new Exception('No "To:" users provided!');
     }
 
     $message_id = $this->getMessageID();
 
     $cc_phids = $this->getCCPHIDs();
     $subject  = $this->buildSubject();
     $body     = $this->buildBody();
 
     $mail = new PhabricatorMetaMTAMail();
     if ($this->getActorID()) {
       $mail->setFrom($this->getActorID());
       $mail->setReplyTo($this->getReplyHandlerEmailAddress());
     } else {
       $mail->setFrom($this->getReplyHandlerEmailAddress());
     }
 
     $mail
       ->addTos($to_phids)
       ->addCCs($cc_phids)
       ->setSubject($subject)
       ->setBody($body)
       ->setIsHTML($this->shouldMarkMailAsHTML())
       ->addHeader('Thread-Topic', $this->getRevision()->getTitle())
       ->addHeader('Thread-Index', $this->generateThreadIndex());
 
     if ($this->isFirstMailAboutRevision()) {
       $mail->addHeader('Message-ID',  $message_id);
     } else {
       $mail->addHeader('In-Reply-To', $message_id);
       $mail->addHeader('References',  $message_id);
     }
 
     if ($this->heraldRulesHeader) {
       $mail->addHeader('X-Herald-Rules', $this->heraldRulesHeader);
     }
 
     $mail->setRelatedPHID($this->getRevision()->getPHID());
 
     // Save this to the MetaMTA queue for later delivery to the MTA.
     $mail->save();
   }
 
   protected function buildSubject() {
     return self::SUBJECT_PREFIX.' '.$this->renderSubject();
   }
 
   protected function shouldMarkMailAsHTML() {
     return false;
   }
 
   protected function buildBody() {
 
     $actions = array();
     $body = $this->renderBody();
     $body .= <<<EOTEXT
 
 ACTIONS
   Reply to comment, or !accept, !reject, !abandon, !resign, or !showdiff.
 
 EOTEXT;
 
     if ($this->getHeraldTranscriptURI() && $this->isFirstMailToRecipients()) {
       $xscript_uri = $this->getHeraldTranscriptURI();
       $body .= <<<EOTEXT
 
 MANAGE HERALD RULES
   http://todo.com/herald/
 
 WHY DID I GET THIS EMAIL?
   {$xscript_uri}
 
 Tip: use the X-Herald-Rules header to filter Herald messages in your client.
 
 EOTEXT;
     }
 
     return $body;
   }
 
   protected function getReplyHandlerEmailAddress() {
     // TODO
     $phid = $this->getRevision()->getPHID();
     $server = 'todo.example.com';
     return "differential+{$phid}@{$server}";
   }
 
   protected function formatText($text) {
     $text = explode("\n", $text);
     foreach ($text as &$line) {
       $line = rtrim('  '.$line);
     }
     unset($line);
     return implode("\n", $text);
   }
 
   public function setToPHIDs(array $to) {
     $this->to = $this->filterContactPHIDs($to);
     return $this;
   }
 
   public function setCCPHIDs(array $cc) {
     $this->cc = $this->filterContactPHIDs($cc);
     return $this;
   }
 
   protected function filterContactPHIDs(array $phids) {
     return $phids;
 
     // TODO: actually do this?
 
     // Differential revisions use Subscriptions for CCs, so any arbitrary
     // PHID can end up CC'd to them. Only try to actually send email PHIDs
     // which have ToolsHandle types that are marked emailable. If we don't
     // filter here, sending the email will fail.
 /*
     $handles = array();
     prep(new ToolsHandleData($phids, $handles));
     foreach ($handles as $phid => $handle) {
       if (!$handle->isEmailable()) {
         unset($handles[$phid]);
       }
     }
     return array_keys($handles);
 */
   }
 
   protected function getToPHIDs() {
     return $this->to;
   }
 
   protected function getCCPHIDs() {
     return $this->cc;
   }
 
   public function setActorID($actor_id) {
     $this->actorID = $actor_id;
     return $this;
   }
 
   public function getActorID() {
     return $this->actorID;
   }
 
   public function setRevision($revision) {
     $this->revision = $revision;
     return $this;
   }
 
   public function getRevision() {
     return $this->revision;
   }
 
   protected function getMessageID() {
     $phid = $this->getRevision()->getPHID();
     // TODO
     return "<differential-rev-{$phid}-req@TODO.com>";
   }
 
-  public function setFeedback($feedback) {
-    $this->feedback = $feedback;
+  public function setComment($comment) {
+    $this->comment = $comment;
     return $this;
   }
 
-  public function getFeedback() {
-    return $this->feedback;
+  public function getComment() {
+    return $this->comment;
   }
 
   public function setChangesets($changesets) {
     $this->changesets = $changesets;
     return $this;
   }
 
   public function getChangesets() {
     return $this->changesets;
   }
 
   public function setInlineComments(array $inline_comments) {
     $this->inlineComments = $inline_comments;
     return $this;
   }
 
   public function getInlineComments() {
     return $this->inlineComments;
   }
 
   public function renderRevisionDetailLink() {
     $uri = $this->getRevisionURI();
     return "REVISION DETAIL\n  {$uri}";
   }
 
   public function getRevisionURI() {
     // TODO
     return 'http://local.aphront.com/D'.$this->getRevision()->getID();
   }
 
   public function setIsFirstMailToRecipients($first) {
     $this->isFirstMailToRecipients = $first;
     return $this;
   }
 
   public function isFirstMailToRecipients() {
     return $this->isFirstMailToRecipients;
   }
 
   public function setIsFirstMailAboutRevision($first) {
     $this->isFirstMailAboutRevision = $first;
     return $this;
   }
 
   public function isFirstMailAboutRevision() {
     return $this->isFirstMailAboutRevision;
   }
 
   protected function generateThreadIndex() {
     // When threading, Outlook ignores the 'References' and 'In-Reply-To'
     // headers that most clients use. Instead, it uses a custom 'Thread-Index'
     // header. The format of this header is something like this (from
     // camel-exchange-folder.c in Evolution Exchange):
 
     /* A new post to a folder gets a 27-byte-long thread index. (The value
      * is apparently unique but meaningless.) Each reply to a post gets a
      * 32-byte-long thread index whose first 27 bytes are the same as the
      * parent's thread index. Each reply to any of those gets a
      * 37-byte-long thread index, etc. The Thread-Index header contains a
      * base64 representation of this value.
      */
 
     // The specific implementation uses a 27-byte header for the first email
     // a recipient receives, and a random 5-byte suffix (32 bytes total)
     // thereafter. This means that all the replies are (incorrectly) siblings,
     // but it would be very difficult to keep track of the entire tree and this
     // gets us reasonable client behavior.
 
     $base = substr(md5($this->getRevision()->getPHID()), 0, 27);
     if (!$this->isFirstMailAboutRevision()) {
       // not totally sure, but it seems like outlook orders replies by
       // thread-index rather than timestamp, so to get these to show up in the
       // right order we use the time as the last 4 bytes.
       $base .= ' ' . pack("N", time());
     }
     return base64_encode($base);
   }
 
   public function setHeraldTranscriptURI($herald_transcript_uri) {
     $this->heraldTranscriptURI = $herald_transcript_uri;
     return $this;
   }
 
   public function getHeraldTranscriptURI() {
     return $this->heraldTranscriptURI;
   }
 
 }
diff --git a/src/applications/differential/mail/feedback/DifferentialFeedbackMail.php b/src/applications/differential/mail/comment/DifferentialCommentMail.php
similarity index 86%
rename from src/applications/differential/mail/feedback/DifferentialFeedbackMail.php
rename to src/applications/differential/mail/comment/DifferentialCommentMail.php
index c3715126f..11ae980ab 100755
--- a/src/applications/differential/mail/feedback/DifferentialFeedbackMail.php
+++ b/src/applications/differential/mail/comment/DifferentialCommentMail.php
@@ -1,114 +1,114 @@
 <?php
 
 /*
  * Copyright 2011 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
-class DifferentialFeedbackMail extends DifferentialMail {
+class DifferentialCommentMail extends DifferentialMail {
 
   protected $changedByCommit;
 
   public function setChangedByCommit($changed_by_commit) {
     $this->changedByCommit = $changed_by_commit;
     return $this;
   }
 
   public function getChangedByCommit() {
     return $this->changedByCommit;
   }
 
   public function __construct(
     DifferentialRevision $revision,
     $actor_id,
-    DifferentialFeedback $feedback,
+    DifferentialComment $comment,
     array $changesets,
     array $inline_comments) {
 
     $this->setRevision($revision);
     $this->setActorID($actor_id);
-    $this->setFeedback($feedback);
+    $this->setComment($comment);
     $this->setChangesets($changesets);
     $this->setInlineComments($inline_comments);
 
   }
 
   protected function renderSubject() {
     $revision = $this->getRevision();
     $verb = $this->getVerb();
-    return ucwords($verb).': '.$revision->getName();
+    return ucwords($verb).': '.$revision->getTitle();
   }
 
   protected function getVerb() {
-    $feedback = $this->getFeedback();
-    $action = $feedback->getAction();
-    $verb = DifferentialAction::getActionVerb($action);
+    $comment = $this->getComment();
+    $action = $comment->getAction();
+    $verb = DifferentialAction::getActionPastTenseVerb($action);
     return $verb;
   }
 
   protected function renderBody() {
 
-    $feedback = $this->getFeedback();
+    $comment = $this->getComment();
 
     $actor = $this->getActorName();
-    $name  = $this->getRevision()->getName();
+    $name  = $this->getRevision()->getTitle();
     $verb  = $this->getVerb();
 
     $body  = array();
 
     $body[] = "{$actor} has {$verb} the revision \"{$name}\".";
     $body[] = null;
 
-    $content = $feedback->getContent();
+    $content = $comment->getContent();
     if (strlen($content)) {
       $body[] = $this->formatText($content);
       $body[] = null;
     }
 
     if ($this->getChangedByCommit()) {
       $body[] = 'CHANGED PRIOR TO COMMIT';
       $body[] = '  This revision was updated prior to commit.';
       $body[] = null;
     }
 
     $inlines = $this->getInlineComments();
     if ($inlines) {
       $body[] = 'INLINE COMMENTS';
       $changesets = $this->getChangesets();
       foreach ($inlines as $inline) {
         $changeset = $changesets[$inline->getChangesetID()];
         if (!$changeset) {
           throw new Exception('Changeset missing!');
         }
         $file = $changeset->getFilename();
         $line = $inline->renderLineRange();
         $content = $inline->getContent();
         $body[] = $this->formatText("{$file}:{$line} {$content}");
       }
       $body[] = null;
     }
 
     $body[] = $this->renderRevisionDetailLink();
     $revision = $this->getRevision();
     if ($revision->getStatus() == DifferentialRevisionStatus::COMMITTED) {
       $rev_ref = $revision->getRevisionRef();
       if ($rev_ref) {
         $body[] = "  Detail URL: ".$rev_ref->getDetailURL();
       }
     }
     $body[] = null;
 
     return implode("\n", $body);
   }
 }
diff --git a/src/applications/differential/mail/feedback/__init__.php b/src/applications/differential/mail/comment/__init__.php
similarity index 86%
rename from src/applications/differential/mail/feedback/__init__.php
rename to src/applications/differential/mail/comment/__init__.php
index dd323e630..38f77fa0c 100644
--- a/src/applications/differential/mail/feedback/__init__.php
+++ b/src/applications/differential/mail/comment/__init__.php
@@ -1,14 +1,14 @@
 <?php
 /**
  * This file is automatically generated. Lint this module to rebuild it.
  * @generated
  */
 
 
 
 phutil_require_module('phabricator', 'applications/differential/constants/action');
 phutil_require_module('phabricator', 'applications/differential/constants/revisionstatus');
 phutil_require_module('phabricator', 'applications/differential/mail/base');
 
 
-phutil_require_source('DifferentialFeedbackMail.php');
+phutil_require_source('DifferentialCommentMail.php');
diff --git a/src/applications/differential/view/addcomment/DifferentialAddCommentView.php b/src/applications/differential/view/addcomment/DifferentialAddCommentView.php
index ec836065b..7d8c135ba 100644
--- a/src/applications/differential/view/addcomment/DifferentialAddCommentView.php
+++ b/src/applications/differential/view/addcomment/DifferentialAddCommentView.php
@@ -1,66 +1,71 @@
 <?php
 
 /*
  * Copyright 2011 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 final class DifferentialAddCommentView extends AphrontView {
 
   private $revision;
   private $actions;
   private $actionURI;
 
   public function setRevision($revision) {
     $this->revision = $revision;
     return $this;
   }
 
   public function setActions(array $actions) {
     $this->actions = $actions;
     return $this;
   }
 
   public function setActionURI($uri) {
     $this->actionURI = $uri;
   }
 
   public function render() {
 
+    $revision = $this->revision;
+
     $actions = array();
     foreach ($this->actions as $action) {
       $actions[$action] = DifferentialAction::getActionVerb($action);
     }
 
     $form = new AphrontFormView();
     $form
       ->setAction($this->actionURI)
+      ->addHiddenInput('revision_id', $revision->getID())
       ->appendChild(
         id(new AphrontFormSelectControl())
           ->setLabel('Action')
+          ->setName('action')
           ->setOptions($actions))
       ->appendChild(
         id(new AphrontFormTextAreaControl())
+          ->setName('comment')
           ->setLabel('Comment'))
       ->appendChild(
         id(new AphrontFormSubmitControl())
           ->setValue('Comment'));
 
     return
       '<div class="differential-panel">'.
         '<h1>Add Comment</h1>'.
         $form->render().
       '</div>';
   }
 }