@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.
- package/README.md +77 -4
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/skills/review-requests/SKILL.md +4 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/skills/analyze/SKILL.md +4 -3
- package/public/css/pr.css +1962 -114
- package/public/js/CONVENTIONS.md +16 -0
- package/public/js/components/AIPanel.js +66 -0
- package/public/js/components/AnalysisConfigModal.js +2 -2
- package/public/js/components/ChatPanel.js +2955 -0
- package/public/js/components/CouncilProgressModal.js +12 -16
- package/public/js/components/KeyboardShortcuts.js +3 -0
- package/public/js/components/PanelGroup.js +723 -0
- package/public/js/components/PreviewModal.js +3 -8
- package/public/js/index.js +8 -0
- package/public/js/local.js +17 -615
- package/public/js/modules/analysis-history.js +19 -68
- package/public/js/modules/comment-manager.js +103 -20
- package/public/js/modules/diff-context.js +176 -0
- package/public/js/modules/diff-renderer.js +30 -0
- package/public/js/modules/file-comment-manager.js +126 -105
- package/public/js/modules/file-list-merger.js +64 -0
- package/public/js/modules/panel-resizer.js +25 -6
- package/public/js/modules/suggestion-manager.js +40 -125
- package/public/js/pr.js +1009 -159
- package/public/js/repo-settings.js +36 -6
- package/public/js/utils/category-emoji.js +44 -0
- package/public/js/utils/time.js +32 -0
- package/public/local.html +107 -70
- package/public/pr.html +107 -70
- package/public/repo-settings.html +32 -0
- package/src/ai/analyzer.js +5 -1
- package/src/ai/copilot-provider.js +39 -9
- package/src/ai/cursor-agent-provider.js +45 -11
- package/src/ai/gemini-provider.js +17 -4
- package/src/ai/prompts/config.js +7 -1
- package/src/ai/provider-availability.js +1 -1
- package/src/ai/provider.js +25 -37
- package/src/chat/CONVENTIONS.md +18 -0
- package/src/chat/pi-bridge.js +491 -0
- package/src/chat/prompt-builder.js +272 -0
- package/src/chat/session-manager.js +619 -0
- package/src/config.js +14 -0
- package/src/database.js +322 -15
- package/src/main.js +4 -17
- package/src/routes/analyses.js +721 -0
- package/src/routes/chat.js +655 -0
- package/src/routes/config.js +29 -8
- package/src/routes/context-files.js +274 -0
- package/src/routes/local.js +225 -1133
- package/src/routes/mcp.js +39 -30
- package/src/routes/pr.js +424 -58
- package/src/routes/reviews.js +1035 -0
- package/src/routes/shared.js +4 -29
- package/src/server.js +34 -12
- package/src/sse/review-events.js +46 -0
- package/src/utils/auto-context.js +88 -0
- package/src/utils/category-emoji.js +33 -0
- package/src/utils/diff-annotator.js +75 -1
- package/src/utils/diff-file-list.js +57 -0
- package/src/routes/analysis.js +0 -1600
- 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 =
|
|
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/
|
|
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 =
|
|
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,
|