@in-the-loop-labs/pair-review 1.6.2 โ†’ 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (63) hide show
  1. package/README.md +77 -4
  2. package/package.json +1 -1
  3. package/plugin/.claude-plugin/plugin.json +1 -1
  4. package/plugin/skills/review-requests/SKILL.md +4 -1
  5. package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
  6. package/plugin-code-critic/skills/analyze/SKILL.md +4 -3
  7. package/public/css/pr.css +1962 -114
  8. package/public/js/CONVENTIONS.md +16 -0
  9. package/public/js/components/AIPanel.js +66 -0
  10. package/public/js/components/AnalysisConfigModal.js +2 -2
  11. package/public/js/components/ChatPanel.js +2955 -0
  12. package/public/js/components/CouncilProgressModal.js +12 -16
  13. package/public/js/components/KeyboardShortcuts.js +3 -0
  14. package/public/js/components/PanelGroup.js +723 -0
  15. package/public/js/components/PreviewModal.js +3 -8
  16. package/public/js/index.js +8 -0
  17. package/public/js/local.js +17 -615
  18. package/public/js/modules/analysis-history.js +19 -68
  19. package/public/js/modules/comment-manager.js +103 -20
  20. package/public/js/modules/diff-context.js +176 -0
  21. package/public/js/modules/diff-renderer.js +30 -0
  22. package/public/js/modules/file-comment-manager.js +126 -105
  23. package/public/js/modules/file-list-merger.js +64 -0
  24. package/public/js/modules/panel-resizer.js +25 -6
  25. package/public/js/modules/suggestion-manager.js +40 -125
  26. package/public/js/pr.js +1009 -159
  27. package/public/js/repo-settings.js +36 -6
  28. package/public/js/utils/category-emoji.js +44 -0
  29. package/public/js/utils/time.js +32 -0
  30. package/public/local.html +107 -70
  31. package/public/pr.html +107 -70
  32. package/public/repo-settings.html +32 -0
  33. package/src/ai/analyzer.js +5 -1
  34. package/src/ai/copilot-provider.js +39 -9
  35. package/src/ai/cursor-agent-provider.js +45 -11
  36. package/src/ai/gemini-provider.js +17 -4
  37. package/src/ai/prompts/config.js +7 -1
  38. package/src/ai/provider-availability.js +1 -1
  39. package/src/ai/provider.js +25 -37
  40. package/src/chat/CONVENTIONS.md +18 -0
  41. package/src/chat/pi-bridge.js +491 -0
  42. package/src/chat/prompt-builder.js +272 -0
  43. package/src/chat/session-manager.js +619 -0
  44. package/src/config.js +14 -0
  45. package/src/database.js +322 -15
  46. package/src/main.js +4 -17
  47. package/src/routes/analyses.js +721 -0
  48. package/src/routes/chat.js +655 -0
  49. package/src/routes/config.js +29 -8
  50. package/src/routes/context-files.js +274 -0
  51. package/src/routes/local.js +225 -1133
  52. package/src/routes/mcp.js +39 -30
  53. package/src/routes/pr.js +424 -58
  54. package/src/routes/reviews.js +1035 -0
  55. package/src/routes/shared.js +4 -29
  56. package/src/server.js +34 -12
  57. package/src/sse/review-events.js +46 -0
  58. package/src/utils/auto-context.js +88 -0
  59. package/src/utils/category-emoji.js +33 -0
  60. package/src/utils/diff-annotator.js +75 -1
  61. package/src/utils/diff-file-list.js +57 -0
  62. package/src/routes/analysis.js +0 -1600
  63. package/src/routes/comments.js +0 -534
package/src/database.js CHANGED
@@ -20,7 +20,7 @@ function getDbPath() {
20
20
  /**
21
21
  * Current schema version - increment this when adding new migrations
22
22
  */
23
- const CURRENT_SCHEMA_VERSION = 19;
23
+ const CURRENT_SCHEMA_VERSION = 24;
24
24
 
25
25
  /**
26
26
  * Database schema SQL statements
@@ -138,6 +138,7 @@ const SCHEMA_SQL = {
138
138
  default_model TEXT,
139
139
  default_council_id TEXT,
140
140
  default_tab TEXT,
141
+ default_chat_instructions TEXT,
141
142
  local_path TEXT,
142
143
  created_at TEXT DEFAULT CURRENT_TIMESTAMP,
143
144
  updated_at TEXT DEFAULT CURRENT_TIMESTAMP
@@ -150,6 +151,7 @@ const SCHEMA_SQL = {
150
151
  review_id INTEGER NOT NULL,
151
152
  provider TEXT,
152
153
  model TEXT,
154
+ tier TEXT,
153
155
  custom_instructions TEXT,
154
156
  repo_instructions TEXT,
155
157
  request_instructions TEXT,
@@ -203,6 +205,47 @@ const SCHEMA_SQL = {
203
205
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
204
206
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
205
207
  )
208
+ `,
209
+
210
+ chat_sessions: `
211
+ CREATE TABLE IF NOT EXISTS chat_sessions (
212
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
213
+ review_id INTEGER NOT NULL,
214
+ context_comment_id INTEGER,
215
+ agent_session_id TEXT, -- Reserved: agent session ID for future reconnection support
216
+ provider TEXT NOT NULL,
217
+ model TEXT,
218
+ status TEXT DEFAULT 'active' CHECK(status IN ('active', 'closed', 'error')),
219
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
220
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
221
+ FOREIGN KEY (review_id) REFERENCES reviews(id),
222
+ FOREIGN KEY (context_comment_id) REFERENCES comments(id)
223
+ )
224
+ `,
225
+
226
+ chat_messages: `
227
+ CREATE TABLE IF NOT EXISTS chat_messages (
228
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
229
+ session_id INTEGER NOT NULL,
230
+ role TEXT NOT NULL,
231
+ type TEXT DEFAULT 'message',
232
+ content TEXT NOT NULL,
233
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
234
+ FOREIGN KEY (session_id) REFERENCES chat_sessions(id) ON DELETE CASCADE
235
+ )
236
+ `,
237
+
238
+ context_files: `
239
+ CREATE TABLE IF NOT EXISTS context_files (
240
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
241
+ review_id INTEGER NOT NULL,
242
+ file TEXT NOT NULL,
243
+ line_start INTEGER NOT NULL,
244
+ line_end INTEGER NOT NULL,
245
+ label TEXT,
246
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
247
+ FOREIGN KEY (review_id) REFERENCES reviews(id) ON DELETE CASCADE
248
+ )
206
249
  `
207
250
  };
208
251
 
@@ -233,7 +276,12 @@ const INDEX_SQL = [
233
276
  'CREATE INDEX IF NOT EXISTS idx_councils_name ON councils(name)',
234
277
  // Voice tracking indexes
235
278
  'CREATE INDEX IF NOT EXISTS idx_comments_voice ON comments(voice_id)',
236
- 'CREATE INDEX IF NOT EXISTS idx_comments_is_raw ON comments(is_raw)'
279
+ 'CREATE INDEX IF NOT EXISTS idx_comments_is_raw ON comments(is_raw)',
280
+ // Chat indexes
281
+ 'CREATE INDEX IF NOT EXISTS idx_chat_sessions_review ON chat_sessions(review_id)',
282
+ 'CREATE INDEX IF NOT EXISTS idx_chat_messages_session ON chat_messages(session_id)',
283
+ // Context files indexes
284
+ 'CREATE INDEX IF NOT EXISTS idx_context_files_review ON context_files(review_id)'
237
285
  ];
238
286
 
239
287
  /**
@@ -942,6 +990,151 @@ const MIGRATIONS = {
942
990
  }
943
991
 
944
992
  console.log('Migration to schema version 19 complete');
993
+ },
994
+
995
+ // Migration to version 20: adds chat_sessions and chat_messages tables
996
+ 20: (db) => {
997
+ console.log('Running migration to schema version 20...');
998
+
999
+ // Create chat_sessions table if it doesn't exist
1000
+ if (!tableExists(db, 'chat_sessions')) {
1001
+ db.exec(`
1002
+ CREATE TABLE chat_sessions (
1003
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1004
+ review_id INTEGER NOT NULL,
1005
+ context_comment_id INTEGER,
1006
+ agent_session_id TEXT, -- Reserved: agent session ID for future reconnection support
1007
+ provider TEXT NOT NULL,
1008
+ model TEXT,
1009
+ status TEXT DEFAULT 'active' CHECK(status IN ('active', 'closed', 'error')),
1010
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
1011
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
1012
+ FOREIGN KEY (review_id) REFERENCES reviews(id),
1013
+ FOREIGN KEY (context_comment_id) REFERENCES comments(id)
1014
+ )
1015
+ `);
1016
+ console.log(' Created chat_sessions table');
1017
+
1018
+ // Create index
1019
+ db.exec('CREATE INDEX IF NOT EXISTS idx_chat_sessions_review ON chat_sessions(review_id)');
1020
+ console.log(' Created index for chat_sessions table');
1021
+ } else {
1022
+ console.log(' Table chat_sessions already exists');
1023
+ }
1024
+
1025
+ // Create chat_messages table if it doesn't exist
1026
+ if (!tableExists(db, 'chat_messages')) {
1027
+ db.exec(`
1028
+ CREATE TABLE chat_messages (
1029
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1030
+ session_id INTEGER NOT NULL,
1031
+ role TEXT NOT NULL,
1032
+ type TEXT DEFAULT 'message',
1033
+ content TEXT NOT NULL,
1034
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
1035
+ FOREIGN KEY (session_id) REFERENCES chat_sessions(id) ON DELETE CASCADE
1036
+ )
1037
+ `);
1038
+ console.log(' Created chat_messages table');
1039
+
1040
+ // Create index
1041
+ db.exec('CREATE INDEX IF NOT EXISTS idx_chat_messages_session ON chat_messages(session_id)');
1042
+ console.log(' Created index for chat_messages table');
1043
+ } else {
1044
+ console.log(' Table chat_messages already exists');
1045
+ }
1046
+
1047
+ console.log('Migration to schema version 20 complete');
1048
+ },
1049
+
1050
+ // Migration to version 21: adds type column to chat_messages for distinguishing context vs message
1051
+ 21: (db) => {
1052
+ console.log('Running migration to schema version 21...');
1053
+
1054
+ const hasType = columnExists(db, 'chat_messages', 'type');
1055
+ if (!hasType) {
1056
+ try {
1057
+ db.prepare(`ALTER TABLE chat_messages ADD COLUMN type TEXT DEFAULT 'message'`).run();
1058
+ console.log(' Added type column to chat_messages');
1059
+ } catch (error) {
1060
+ if (!error.message.includes('duplicate column name')) {
1061
+ throw error;
1062
+ }
1063
+ console.log(' Column type already exists (race condition)');
1064
+ }
1065
+ } else {
1066
+ console.log(' Column type already exists');
1067
+ }
1068
+
1069
+ console.log('Migration to schema version 21 complete');
1070
+ },
1071
+
1072
+ 22: (db) => {
1073
+ console.log('Migrating to schema version 22: Add tier column to analysis_runs');
1074
+
1075
+ const columns = db.prepare('PRAGMA table_info(analysis_runs)').all();
1076
+ if (!columns.some(c => c.name === 'tier')) {
1077
+ try {
1078
+ db.prepare('ALTER TABLE analysis_runs ADD COLUMN tier TEXT').run();
1079
+ console.log(' Added tier column to analysis_runs');
1080
+ } catch (error) {
1081
+ if (!error.message.includes('duplicate column name')) {
1082
+ throw error;
1083
+ }
1084
+ console.log(' Column tier already exists (race condition)');
1085
+ }
1086
+ } else {
1087
+ console.log(' Column tier already exists');
1088
+ }
1089
+
1090
+ console.log('Migration to schema version 22 complete');
1091
+ },
1092
+
1093
+ 23: (db) => {
1094
+ console.log('Migrating to schema version 23: Add default_chat_instructions to repo_settings');
1095
+
1096
+ const columns = db.prepare('PRAGMA table_info(repo_settings)').all();
1097
+ if (!columns.some(c => c.name === 'default_chat_instructions')) {
1098
+ try {
1099
+ db.prepare('ALTER TABLE repo_settings ADD COLUMN default_chat_instructions TEXT').run();
1100
+ console.log(' Added default_chat_instructions column to repo_settings');
1101
+ } catch (error) {
1102
+ if (!error.message.includes('duplicate column name')) {
1103
+ throw error;
1104
+ }
1105
+ console.log(' Column default_chat_instructions already exists (race condition)');
1106
+ }
1107
+ } else {
1108
+ console.log(' Column default_chat_instructions already exists');
1109
+ }
1110
+
1111
+ console.log('Migration to schema version 23 complete');
1112
+ },
1113
+
1114
+ // Migration to version 24: adds context_files table for pinning non-diff file ranges to the diff panel
1115
+ 24: (db) => {
1116
+ console.log('Migrating to schema version 24: Add context_files table');
1117
+
1118
+ if (!tableExists(db, 'context_files')) {
1119
+ db.exec(`
1120
+ CREATE TABLE IF NOT EXISTS context_files (
1121
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
1122
+ review_id INTEGER NOT NULL,
1123
+ file TEXT NOT NULL,
1124
+ line_start INTEGER NOT NULL,
1125
+ line_end INTEGER NOT NULL,
1126
+ label TEXT,
1127
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
1128
+ FOREIGN KEY (review_id) REFERENCES reviews(id) ON DELETE CASCADE
1129
+ )
1130
+ `);
1131
+ db.exec('CREATE INDEX IF NOT EXISTS idx_context_files_review ON context_files(review_id)');
1132
+ console.log(' Created context_files table');
1133
+ } else {
1134
+ console.log(' Table context_files already exists');
1135
+ }
1136
+
1137
+ console.log('Migration to schema version 24 complete');
945
1138
  }
946
1139
  };
947
1140
 
@@ -1441,7 +1634,7 @@ class RepoSettingsRepository {
1441
1634
  */
1442
1635
  async getRepoSettings(repository) {
1443
1636
  const row = await queryOne(this.db, `
1444
- SELECT id, repository, default_instructions, default_provider, default_model, default_council_id, default_tab, local_path, created_at, updated_at
1637
+ SELECT id, repository, default_instructions, default_provider, default_model, default_council_id, default_tab, default_chat_instructions, local_path, created_at, updated_at
1445
1638
  FROM repo_settings
1446
1639
  WHERE repository = ? COLLATE NOCASE
1447
1640
  `, [repository]);
@@ -1498,7 +1691,7 @@ class RepoSettingsRepository {
1498
1691
  * @returns {Promise<Object>} Saved settings object
1499
1692
  */
1500
1693
  async saveRepoSettings(repository, settings) {
1501
- const { default_instructions, default_provider, default_model, default_council_id, default_tab, local_path } = settings;
1694
+ const { default_instructions, default_provider, default_model, default_council_id, default_tab, default_chat_instructions, local_path } = settings;
1502
1695
  const now = new Date().toISOString();
1503
1696
 
1504
1697
  // Check if settings already exist
@@ -1513,6 +1706,7 @@ class RepoSettingsRepository {
1513
1706
  default_model = ?,
1514
1707
  default_council_id = ?,
1515
1708
  default_tab = ?,
1709
+ default_chat_instructions = ?,
1516
1710
  local_path = ?,
1517
1711
  updated_at = ?
1518
1712
  WHERE repository = ? COLLATE NOCASE
@@ -1522,6 +1716,7 @@ class RepoSettingsRepository {
1522
1716
  default_model !== undefined ? default_model : existing.default_model,
1523
1717
  default_council_id !== undefined ? default_council_id : existing.default_council_id,
1524
1718
  default_tab !== undefined ? default_tab : existing.default_tab,
1719
+ default_chat_instructions !== undefined ? default_chat_instructions : existing.default_chat_instructions,
1525
1720
  local_path !== undefined ? local_path : existing.local_path,
1526
1721
  now,
1527
1722
  repository
@@ -1534,15 +1729,16 @@ class RepoSettingsRepository {
1534
1729
  default_model: default_model !== undefined ? default_model : existing.default_model,
1535
1730
  default_council_id: default_council_id !== undefined ? default_council_id : existing.default_council_id,
1536
1731
  default_tab: default_tab !== undefined ? default_tab : existing.default_tab,
1732
+ default_chat_instructions: default_chat_instructions !== undefined ? default_chat_instructions : existing.default_chat_instructions,
1537
1733
  local_path: local_path !== undefined ? local_path : existing.local_path,
1538
1734
  updated_at: now
1539
1735
  };
1540
1736
  } else {
1541
1737
  // Insert new settings
1542
1738
  const result = await run(this.db, `
1543
- INSERT INTO repo_settings (repository, default_instructions, default_provider, default_model, default_council_id, default_tab, local_path, created_at, updated_at)
1544
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
1545
- `, [repository, default_instructions || null, default_provider || null, default_model || null, default_council_id || null, default_tab || null, local_path || null, now, now]);
1739
+ INSERT INTO repo_settings (repository, default_instructions, default_provider, default_model, default_council_id, default_tab, default_chat_instructions, local_path, created_at, updated_at)
1740
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
1741
+ `, [repository, default_instructions || null, default_provider || null, default_model || null, default_council_id || null, default_tab || null, default_chat_instructions || null, local_path || null, now, now]);
1546
1742
 
1547
1743
  return {
1548
1744
  id: result.lastID,
@@ -1552,6 +1748,7 @@ class RepoSettingsRepository {
1552
1748
  default_model: default_model || null,
1553
1749
  default_council_id: default_council_id || null,
1554
1750
  default_tab: default_tab || null,
1751
+ default_chat_instructions: default_chat_instructions || null,
1555
1752
  local_path: local_path || null,
1556
1753
  created_at: now,
1557
1754
  updated_at: now
@@ -2159,7 +2356,8 @@ class ReviewRepository {
2159
2356
  async getReview(id) {
2160
2357
  const row = await queryOne(this.db, `
2161
2358
  SELECT id, pr_number, repository, status, review_id,
2162
- created_at, updated_at, submitted_at, review_data, custom_instructions, summary
2359
+ created_at, updated_at, submitted_at, review_data, custom_instructions, summary,
2360
+ review_type, local_path, local_head_sha
2163
2361
  FROM reviews
2164
2362
  WHERE id = ?
2165
2363
  `, [id]);
@@ -2631,14 +2829,14 @@ class AnalysisRunRepository {
2631
2829
  * @param {string} [runInfo.status='running'] - Initial status (default 'running'; pass 'completed' for externally-produced results)
2632
2830
  * @returns {Promise<Object>} Created analysis run record
2633
2831
  */
2634
- async create({ id, reviewId, provider = null, model = null, customInstructions = null, repoInstructions = null, requestInstructions = null, headSha = null, status = 'running', parentRunId = null, configType = 'single', levelsConfig = null }) {
2832
+ async create({ id, reviewId, provider = null, model = null, tier = null, customInstructions = null, repoInstructions = null, requestInstructions = null, headSha = null, status = 'running', parentRunId = null, configType = 'single', levelsConfig = null }) {
2635
2833
  const isTerminal = ['completed', 'failed', 'cancelled'].includes(status);
2636
2834
  const completedAt = isTerminal ? 'CURRENT_TIMESTAMP' : 'NULL';
2637
2835
  const levelsConfigJson = levelsConfig ? JSON.stringify(levelsConfig) : null;
2638
2836
  await run(this.db, `
2639
- INSERT INTO analysis_runs (id, review_id, provider, model, custom_instructions, repo_instructions, request_instructions, head_sha, status, completed_at, parent_run_id, config_type, levels_config)
2640
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ${completedAt}, ?, ?, ?)
2641
- `, [id, reviewId, provider, model, customInstructions, repoInstructions, requestInstructions, headSha, status, parentRunId, configType, levelsConfigJson]);
2837
+ INSERT INTO analysis_runs (id, review_id, provider, model, tier, custom_instructions, repo_instructions, request_instructions, head_sha, status, completed_at, parent_run_id, config_type, levels_config)
2838
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ${completedAt}, ?, ?, ?)
2839
+ `, [id, reviewId, provider, model, tier, customInstructions, repoInstructions, requestInstructions, headSha, status, parentRunId, configType, levelsConfigJson]);
2642
2840
 
2643
2841
  // Query back the inserted row to return actual database values (including timestamps)
2644
2842
  return await this.getById(id);
@@ -2713,7 +2911,7 @@ class AnalysisRunRepository {
2713
2911
  */
2714
2912
  async getById(id) {
2715
2913
  const row = await queryOne(this.db, `
2716
- SELECT id, review_id, provider, model, custom_instructions, repo_instructions, request_instructions,
2914
+ SELECT id, review_id, provider, model, tier, custom_instructions, repo_instructions, request_instructions,
2717
2915
  head_sha, summary, status, total_suggestions, files_analyzed, started_at, completed_at,
2718
2916
  parent_run_id, config_type, levels_config
2719
2917
  FROM analysis_runs
@@ -2734,7 +2932,7 @@ class AnalysisRunRepository {
2734
2932
  async getByReviewId(reviewId, { limit } = {}) {
2735
2933
  const params = [reviewId];
2736
2934
  let sql = `
2737
- SELECT id, review_id, provider, model, custom_instructions, repo_instructions, request_instructions,
2935
+ SELECT id, review_id, provider, model, tier, custom_instructions, repo_instructions, request_instructions,
2738
2936
  head_sha, summary, status, total_suggestions, files_analyzed, started_at, completed_at,
2739
2937
  parent_run_id, config_type, levels_config
2740
2938
  FROM analysis_runs
@@ -2764,7 +2962,7 @@ class AnalysisRunRepository {
2764
2962
  */
2765
2963
  async getChildRuns(parentRunId) {
2766
2964
  return query(this.db, `
2767
- SELECT id, review_id, provider, model, custom_instructions, repo_instructions, request_instructions,
2965
+ SELECT id, review_id, provider, model, tier, custom_instructions, repo_instructions, request_instructions,
2768
2966
  head_sha, summary, status, total_suggestions, files_analyzed, started_at, completed_at,
2769
2967
  parent_run_id, config_type, levels_config
2770
2968
  FROM analysis_runs
@@ -3128,6 +3326,114 @@ class CouncilRepository {
3128
3326
  }
3129
3327
  }
3130
3328
 
3329
+ /**
3330
+ * ContextFileRepository class for managing context file range records.
3331
+ * Context files allow pinning specific line ranges from non-diff files
3332
+ * into the diff panel for review.
3333
+ */
3334
+ class ContextFileRepository {
3335
+ /**
3336
+ * Create a new ContextFileRepository instance
3337
+ * @param {Database} db - Database instance
3338
+ */
3339
+ constructor(db) {
3340
+ this.db = db;
3341
+ }
3342
+
3343
+ /**
3344
+ * Add a context file range for a review
3345
+ * @param {number} reviewId - Review ID
3346
+ * @param {string} file - File path
3347
+ * @param {number} lineStart - Start line number
3348
+ * @param {number} lineEnd - End line number
3349
+ * @param {string|null} [label=null] - Optional label for the range
3350
+ * @returns {Promise<Object>} The newly created context file record
3351
+ */
3352
+ async add(reviewId, file, lineStart, lineEnd, label = null) {
3353
+ const result = await run(this.db, `
3354
+ INSERT INTO context_files (review_id, file, line_start, line_end, label)
3355
+ VALUES (?, ?, ?, ?, ?)
3356
+ `, [reviewId, file, lineStart, lineEnd, label]);
3357
+
3358
+ return queryOne(this.db, `
3359
+ SELECT id, review_id, file, line_start, line_end, label, created_at
3360
+ FROM context_files
3361
+ WHERE id = ?
3362
+ `, [result.lastID]);
3363
+ }
3364
+
3365
+ /**
3366
+ * Get all context file ranges for a review, ordered by id
3367
+ * @param {number} reviewId - Review ID
3368
+ * @returns {Promise<Array<Object>>} Array of context file records
3369
+ */
3370
+ async getByReviewId(reviewId) {
3371
+ return query(this.db, `
3372
+ SELECT id, review_id, file, line_start, line_end, label, created_at
3373
+ FROM context_files
3374
+ WHERE review_id = ?
3375
+ ORDER BY id
3376
+ `, [reviewId]);
3377
+ }
3378
+
3379
+ /**
3380
+ * Get context file ranges for a specific file within a review, ordered by line_start
3381
+ * @param {number} reviewId - Review ID
3382
+ * @param {string} file - File path
3383
+ * @returns {Promise<Array<Object>>} Array of context file records
3384
+ */
3385
+ async getByReviewIdAndFile(reviewId, file) {
3386
+ return query(this.db, `
3387
+ SELECT id, review_id, file, line_start, line_end, label, created_at
3388
+ FROM context_files
3389
+ WHERE review_id = ? AND file = ?
3390
+ ORDER BY line_start
3391
+ `, [reviewId, file]);
3392
+ }
3393
+
3394
+ /**
3395
+ * Update the line range of an existing context file record
3396
+ * @param {number} id - Context file record ID
3397
+ * @param {number} lineStart - New start line number
3398
+ * @param {number} lineEnd - New end line number
3399
+ * @returns {Promise<boolean>} True if record was updated
3400
+ */
3401
+ async updateRange(id, lineStart, lineEnd) {
3402
+ const result = await run(this.db, `
3403
+ UPDATE context_files SET line_start = ?, line_end = ? WHERE id = ?
3404
+ `, [lineStart, lineEnd, id]);
3405
+
3406
+ return result.changes > 0;
3407
+ }
3408
+
3409
+ /**
3410
+ * Remove a context file range by ID, scoped to a specific review
3411
+ * @param {number} id - Context file record ID
3412
+ * @param {number} reviewId - Review ID (ensures deletion is scoped to the correct review)
3413
+ * @returns {Promise<boolean>} True if record was deleted
3414
+ */
3415
+ async remove(id, reviewId) {
3416
+ const result = await run(this.db, `
3417
+ DELETE FROM context_files WHERE id = ? AND review_id = ?
3418
+ `, [id, reviewId]);
3419
+
3420
+ return result.changes > 0;
3421
+ }
3422
+
3423
+ /**
3424
+ * Remove all context file ranges for a review
3425
+ * @param {number} reviewId - Review ID
3426
+ * @returns {Promise<number>} Number of records deleted
3427
+ */
3428
+ async removeAll(reviewId) {
3429
+ const result = await run(this.db, `
3430
+ DELETE FROM context_files WHERE review_id = ?
3431
+ `, [reviewId]);
3432
+
3433
+ return result.changes;
3434
+ }
3435
+ }
3436
+
3131
3437
  module.exports = {
3132
3438
  initializeDatabase,
3133
3439
  closeDatabase,
@@ -3150,6 +3456,7 @@ module.exports = {
3150
3456
  AnalysisRunRepository,
3151
3457
  GitHubReviewRepository,
3152
3458
  CouncilRepository,
3459
+ ContextFileRepository,
3153
3460
  generateWorktreeId,
3154
3461
  migrateExistingWorktrees
3155
3462
  };
package/src/main.js CHANGED
@@ -14,6 +14,7 @@ const { normalizeRepository, resolveRenamedFile, resolveRenamedFileOld } = requi
14
14
  const logger = require('./utils/logger');
15
15
  const simpleGit = require('simple-git');
16
16
  const { getGeneratedFilePatterns } = require('./git/gitattributes');
17
+ const { getEmoji: getCategoryEmoji } = require('./utils/category-emoji');
17
18
  const open = (...args) => import('open').then(({default: open}) => open(...args));
18
19
 
19
20
  let db = null;
@@ -554,7 +555,7 @@ async function handlePullRequest(args, config, db, flags = {}) {
554
555
  await new Promise(resolve => setTimeout(resolve, retryDelay * attempt));
555
556
  }
556
557
 
557
- const response = await fetch(`http://localhost:${port}/api/analyze/${prInfo.owner}/${prInfo.repo}/${prInfo.number}`, {
558
+ const response = await fetch(`http://localhost:${port}/api/pr/${prInfo.owner}/${prInfo.repo}/${prInfo.number}/analyses`, {
558
559
  method: 'POST',
559
560
  headers: { 'Content-Type': 'application/json' }
560
561
  });
@@ -657,20 +658,6 @@ async function startServerWithPRContext(config, prInfo, flags = {}) {
657
658
  return actualPort;
658
659
  }
659
660
 
660
- /**
661
- * Category to emoji mapping for AI suggestions
662
- */
663
- const CATEGORY_EMOJI_MAP = {
664
- 'bug': '๐Ÿ›',
665
- 'performance': 'โšก',
666
- 'design': '๐Ÿ“',
667
- 'code-style': '๐Ÿงน',
668
- 'improvement': '๐Ÿ’ก',
669
- 'praise': 'โญ',
670
- 'security': '๐Ÿ”’',
671
- 'suggestion': '๐Ÿ’ฌ'
672
- };
673
-
674
661
  /**
675
662
  * Format AI suggestion with emoji and category prefix
676
663
  * @param {string} text - The suggestion text
@@ -681,7 +668,7 @@ function formatAISuggestion(text, category) {
681
668
  if (!category) {
682
669
  return text;
683
670
  }
684
- const emoji = CATEGORY_EMOJI_MAP[category] || '๐Ÿ’ฌ';
671
+ const emoji = getCategoryEmoji(category);
685
672
  // Properly capitalize hyphenated categories (e.g., "code-style" -> "Code Style")
686
673
  const capitalizedCategory = category
687
674
  .split('-')
@@ -734,6 +721,7 @@ async function performHeadlessReview(args, config, db, flags, options) {
734
721
  let worktreePath;
735
722
  let diff;
736
723
  let changedFiles;
724
+ const repository = normalizeRepository(prInfo.owner, prInfo.repo);
737
725
 
738
726
  // Determine working directory: --use-checkout uses current directory
739
727
  if (flags.useCheckout) {
@@ -803,7 +791,6 @@ async function performHeadlessReview(args, config, db, flags, options) {
803
791
  } else {
804
792
  // Current directory is not the target repository - find or clone it
805
793
  console.log(`Current directory is not a checkout of ${prInfo.owner}/${prInfo.repo}, locating repository...`);
806
- const repository = normalizeRepository(prInfo.owner, prInfo.repo);
807
794
  const result = await findRepositoryPath({
808
795
  db,
809
796
  owner: prInfo.owner,