@in-the-loop-labs/pair-review 2.2.0 → 2.3.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 -0
- package/package.json +3 -2
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin-code-critic/.claude-plugin/plugin.json +1 -1
- package/public/js/components/ChatPanel.js +164 -100
- package/public/js/components/CouncilProgressModal.js +56 -64
- package/public/js/components/PanelGroup.js +4 -2
- package/public/js/modules/file-comment-manager.js +11 -30
- package/public/js/modules/panel-resizer.js +84 -1
- package/public/js/modules/suggestion-manager.js +2 -23
- package/public/js/pr.js +20 -15
- package/public/js/ws-client.js +155 -0
- package/public/local.html +3 -0
- package/public/pr.html +3 -0
- package/public/setup.html +51 -90
- package/src/ai/analyzer.js +5 -8
- package/src/config.js +70 -49
- package/src/database.js +29 -9
- package/src/events/review-events.js +30 -0
- package/src/routes/analyses.js +3 -102
- package/src/routes/chat.js +37 -74
- package/src/routes/config.js +62 -4
- package/src/routes/context-files.js +1 -1
- package/src/routes/local.js +1 -2
- package/src/routes/mcp.js +1 -1
- package/src/routes/pr.js +1 -2
- package/src/routes/reviews.js +36 -29
- package/src/routes/setup.js +17 -114
- package/src/routes/shared.js +5 -49
- package/src/server.js +4 -0
- package/src/utils/comment-formatter.js +137 -0
- package/src/ws/index.js +2 -0
- package/src/ws/server.js +123 -0
- package/src/sse/review-events.js +0 -46
package/README.md
CHANGED
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
- [Customization](#customization)
|
|
32
32
|
- [Review Feedback Export](#review-feedback-export)
|
|
33
33
|
- [Inline Comments](#inline-comments)
|
|
34
|
+
- [Comment Format](#comment-format)
|
|
34
35
|
- [Local Mode](#local-mode)
|
|
35
36
|
- [Claude Code Plugins](#claude-code-plugins)
|
|
36
37
|
- [MCP Integration](#mcp-integration)
|
|
@@ -235,6 +236,22 @@ On first run, pair-review creates `~/.pair-review/config.example.json` with comp
|
|
|
235
236
|
|
|
236
237
|
For advanced configuration with custom providers and models, see [AI Provider Configuration](#ai-provider-configuration) below.
|
|
237
238
|
|
|
239
|
+
### Configuration Files
|
|
240
|
+
|
|
241
|
+
pair-review loads configuration from multiple files, merged in order of increasing precedence:
|
|
242
|
+
|
|
243
|
+
| Priority | File | Purpose |
|
|
244
|
+
|----------|------|---------|
|
|
245
|
+
| 1 (lowest) | Built-in defaults | Sensible defaults for all settings |
|
|
246
|
+
| 2 | `~/.pair-review/config.json` | Global user configuration |
|
|
247
|
+
| 3 | `~/.pair-review/config.local.json` | Personal overrides (gitignored) |
|
|
248
|
+
| 4 | `.pair-review/config.json` | Project-specific configuration (can be checked in) |
|
|
249
|
+
| 5 (highest) | `.pair-review/config.local.json` | Personal project overrides (gitignored) |
|
|
250
|
+
|
|
251
|
+
Nested objects (like `chat`, `providers`, `monorepos`) are deep-merged across layers — you only need to specify the keys you want to override.
|
|
252
|
+
|
|
253
|
+
**`config.local.json`** files are intended for personal overrides that should not be committed to version control. Add `config.local.json` to your `.gitignore`.
|
|
254
|
+
|
|
238
255
|
### Environment Variables
|
|
239
256
|
|
|
240
257
|
pair-review supports several environment variables for customizing behavior:
|
|
@@ -484,6 +501,66 @@ The markdown includes file paths, line numbers, and your comments - everything t
|
|
|
484
501
|
- Edit or discard AI suggestions before finalizing
|
|
485
502
|
- Comments include file and line number for precision
|
|
486
503
|
|
|
504
|
+
### Comment Format
|
|
505
|
+
|
|
506
|
+
When AI suggestions are adopted as review comments, pair-review formats them with an emoji and category prefix by default. You can customize this format via the `comment_format` setting in `~/.pair-review/config.json`.
|
|
507
|
+
|
|
508
|
+
**Presets:**
|
|
509
|
+
|
|
510
|
+
| Preset | Template | Example Output (without suggestion)|
|
|
511
|
+
|--------|----------|---------------|
|
|
512
|
+
| `legacy` | `{emoji} **{category}**: {description}{?suggestion}\n\n**Suggestion:** {suggestion}{/suggestion}` | 🐛 **Bug**: Missing null check |
|
|
513
|
+
| `minimal` | `[{category}] {description}{?suggestion}\n\n{suggestion}{/suggestion}` | [Bug] Missing null check |
|
|
514
|
+
| `plain` | `{description}{?suggestion}\n\n{suggestion}{/suggestion}` | Missing null check |
|
|
515
|
+
| `emoji-only` | `{emoji} {description}{?suggestion}\n\n{suggestion}{/suggestion}` | 🐛 Missing null check |
|
|
516
|
+
| `maximal` | `{emoji} **{category}**{?title}: {title}{/title}\n\n{description}{?suggestion}\n\n**Suggestion:** {suggestion}{/suggestion}` | 🐛 **Bug**: Null Safety Issue (includes title) |
|
|
517
|
+
|
|
518
|
+
To use a preset:
|
|
519
|
+
|
|
520
|
+
```json
|
|
521
|
+
{
|
|
522
|
+
"comment_format": "minimal"
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
**Custom templates:**
|
|
527
|
+
|
|
528
|
+
You can provide a custom template using these placeholders: `{emoji}`, `{category}`, `{title}`, `{description}`, `{suggestion}`.
|
|
529
|
+
|
|
530
|
+
**Conditional sections:** Use `{?field}...{/field}` to conditionally include content. When the field value is truthy, the delimiters are stripped and the content is kept. When the field is empty/null/undefined, the entire block (including surrounding text within the delimiters) is removed. For example, `{?suggestion}\n\n**Suggestion:** {suggestion}{/suggestion}` will omit the entire suggestion line when there is no remediation text.
|
|
531
|
+
|
|
532
|
+
```json
|
|
533
|
+
{
|
|
534
|
+
"comment_format": {
|
|
535
|
+
"template": "{emoji} **{category}**: {description}{?suggestion}\n\n**Suggestion:** {suggestion}{/suggestion}",
|
|
536
|
+
"emojiOverrides": {
|
|
537
|
+
"bug": "🔴"
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
**Category overrides:**
|
|
544
|
+
|
|
545
|
+
Use `categoryOverrides` to rename categories in the formatted output. This is a string-to-string mapping where keys are the original category names (lowercase) and values are the replacement names.
|
|
546
|
+
|
|
547
|
+
```json
|
|
548
|
+
{
|
|
549
|
+
"comment_format": {
|
|
550
|
+
"template": "{emoji} **{category}**: {description}{?suggestion}\n\n**Suggestion:** {suggestion}{/suggestion}",
|
|
551
|
+
"categoryOverrides": { "bug": "defect", "performance": "perf" }
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
```
|
|
555
|
+
|
|
556
|
+
- `{title}` uses the suggestion's title field if available.
|
|
557
|
+
- `emojiOverrides` lets you replace the default emoji for specific categories.
|
|
558
|
+
- `categoryOverrides` lets you rename categories (e.g., "bug" to "defect").
|
|
559
|
+
|
|
560
|
+
Templates typically include `{description}` to render the suggestion body.
|
|
561
|
+
|
|
562
|
+
**Builtin categories:** bug, improvement, praise, suggestion, design, performance, security, code-style
|
|
563
|
+
|
|
487
564
|
### Local Mode
|
|
488
565
|
|
|
489
566
|
Review **unstaged**, uncommitted changes before creating a PR:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@in-the-loop-labs/pair-review",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"description": "Your AI-powered code review partner - Close the feedback loop with AI coding agents",
|
|
5
5
|
"main": "src/server.js",
|
|
6
6
|
"bin": {
|
|
@@ -67,7 +67,8 @@
|
|
|
67
67
|
"open": "^9.1.0",
|
|
68
68
|
"simple-git": "^3.19.1",
|
|
69
69
|
"update-notifier": "^5.1.0",
|
|
70
|
-
"uuid": "^11.1.0"
|
|
70
|
+
"uuid": "^11.1.0",
|
|
71
|
+
"ws": "^8.19.0"
|
|
71
72
|
},
|
|
72
73
|
"devDependencies": {
|
|
73
74
|
"@changesets/cli": "^2.29.8",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pair-review",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"description": "pair-review app integration — Open PRs and local changes in the pair-review web UI, run server-side AI analysis, and address review feedback. Requires the pair-review MCP server.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "in-the-loop-labs",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-critic",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"description": "AI-powered code review analysis — Run three-level AI analysis and implement-review-fix loops directly in your coding agent. Works standalone, no server required.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "in-the-loop-labs",
|
|
@@ -18,8 +18,8 @@ class ChatPanel {
|
|
|
18
18
|
this.reviewId = null;
|
|
19
19
|
this.isOpen = false;
|
|
20
20
|
this.isStreaming = false;
|
|
21
|
-
this.
|
|
22
|
-
this.
|
|
21
|
+
this._chatUnsub = null;
|
|
22
|
+
this._reviewUnsub = null;
|
|
23
23
|
this.messages = [];
|
|
24
24
|
this._streamingContent = '';
|
|
25
25
|
this._pendingContext = [];
|
|
@@ -28,7 +28,7 @@ class ChatPanel {
|
|
|
28
28
|
this._contextItemId = null; // suggestion ID or comment ID from context
|
|
29
29
|
this._contextLineMeta = null; // { file, line_start, line_end } — set when opened with line context
|
|
30
30
|
this._pendingActionContext = null; // { type, itemId } — set by action button handlers, consumed by sendMessage
|
|
31
|
-
this._resizeConfig =
|
|
31
|
+
this._resizeConfig = ChatPanel.RESIZE_CONFIG;
|
|
32
32
|
this._analysisContextRemoved = false;
|
|
33
33
|
this._sessionAnalysisRunId = null; // tracks which AI run ID's context is loaded in the current session
|
|
34
34
|
this._openPromise = null; // concurrency guard for open()
|
|
@@ -434,8 +434,8 @@ class ChatPanel {
|
|
|
434
434
|
this.panel.classList.remove('chat-panel--closed');
|
|
435
435
|
this.panel.classList.add('chat-panel--open');
|
|
436
436
|
|
|
437
|
-
// Ensure
|
|
438
|
-
this.
|
|
437
|
+
// Ensure WebSocket subscriptions are active (but don't create a session yet — lazy creation)
|
|
438
|
+
this._ensureSubscriptions();
|
|
439
439
|
|
|
440
440
|
// Load MRU session with message history (if any previous sessions exist).
|
|
441
441
|
// Skip when opening with explicit context (suggestion/comment/file) — the
|
|
@@ -495,7 +495,7 @@ class ChatPanel {
|
|
|
495
495
|
close() {
|
|
496
496
|
this._hideSessionDropdown();
|
|
497
497
|
// Reset UI streaming state (buttons) but keep isStreaming and _streamingContent
|
|
498
|
-
// intact so the background
|
|
498
|
+
// intact so the background WebSocket handler can continue accumulating events.
|
|
499
499
|
this.sendBtn.style.display = '';
|
|
500
500
|
this.stopBtn.style.display = 'none';
|
|
501
501
|
this.sendBtn.disabled = !this.inputEl?.value?.trim();
|
|
@@ -544,6 +544,7 @@ class ChatPanel {
|
|
|
544
544
|
// 2. Clear everything as normal
|
|
545
545
|
this._finalizeStreaming();
|
|
546
546
|
this.currentSessionId = null;
|
|
547
|
+
this._resubscribeChat(); // Unsubscribe old chat topic
|
|
547
548
|
this.messages = [];
|
|
548
549
|
this._streamingContent = '';
|
|
549
550
|
this._pendingContext = [];
|
|
@@ -556,7 +557,6 @@ class ChatPanel {
|
|
|
556
557
|
this._clearMessages();
|
|
557
558
|
this._updateActionButtons();
|
|
558
559
|
this._updateTitle(); // Reset title for new conversation
|
|
559
|
-
// SSE stays connected — it's multiplexed and will filter by sessionId
|
|
560
560
|
|
|
561
561
|
// 3. Re-add analysis context (appears first, handled separately from pending arrays)
|
|
562
562
|
this._ensureAnalysisContext();
|
|
@@ -626,6 +626,7 @@ class ChatPanel {
|
|
|
626
626
|
|
|
627
627
|
const mru = sessions[0];
|
|
628
628
|
this.currentSessionId = mru.id;
|
|
629
|
+
this._resubscribeChat();
|
|
629
630
|
console.debug('[ChatPanel] Loaded MRU session:', mru.id, 'messages:', mru.message_count);
|
|
630
631
|
|
|
631
632
|
if (mru.provider) {
|
|
@@ -786,6 +787,7 @@ class ChatPanel {
|
|
|
786
787
|
|
|
787
788
|
// 2. Reset state
|
|
788
789
|
this.currentSessionId = sessionId;
|
|
790
|
+
this._resubscribeChat();
|
|
789
791
|
this.messages = [];
|
|
790
792
|
this._streamingContent = '';
|
|
791
793
|
this._pendingContext = [];
|
|
@@ -869,12 +871,12 @@ class ChatPanel {
|
|
|
869
871
|
}
|
|
870
872
|
|
|
871
873
|
/**
|
|
872
|
-
* Ensure
|
|
874
|
+
* Ensure WebSocket subscriptions are established for review and chat topics.
|
|
873
875
|
* No longer creates sessions — that happens lazily on first message.
|
|
874
876
|
* @returns {{sessionData: null}}
|
|
875
877
|
*/
|
|
876
878
|
_ensureConnected() {
|
|
877
|
-
this.
|
|
879
|
+
this._ensureSubscriptions();
|
|
878
880
|
return { sessionData: null };
|
|
879
881
|
}
|
|
880
882
|
|
|
@@ -893,6 +895,12 @@ class ChatPanel {
|
|
|
893
895
|
this.reviewId = reviewId;
|
|
894
896
|
console.debug('[ChatPanel] Late-bound reviewId:', reviewId);
|
|
895
897
|
|
|
898
|
+
// Subscribe to review topic now that reviewId is available.
|
|
899
|
+
// _ensureSubscriptions() skips this when reviewId is null at panel open time,
|
|
900
|
+
// so we must subscribe here. The chat subscription is a benign no-op when
|
|
901
|
+
// currentSessionId is null.
|
|
902
|
+
this._ensureSubscriptions();
|
|
903
|
+
|
|
896
904
|
// Re-enable input now that reviewId is available
|
|
897
905
|
if (this.inputEl.disabled) {
|
|
898
906
|
this._enableInput();
|
|
@@ -942,6 +950,7 @@ class ChatPanel {
|
|
|
942
950
|
|
|
943
951
|
const result = await response.json();
|
|
944
952
|
this.currentSessionId = result.data.id;
|
|
953
|
+
this._resubscribeChat();
|
|
945
954
|
console.debug('[ChatPanel] Session created:', this.currentSessionId);
|
|
946
955
|
return result.data;
|
|
947
956
|
} catch (error) {
|
|
@@ -975,7 +984,7 @@ class ChatPanel {
|
|
|
975
984
|
|
|
976
985
|
// Lazy session creation: create on first message, not on panel open
|
|
977
986
|
if (!this.currentSessionId) {
|
|
978
|
-
this.
|
|
987
|
+
this._ensureSubscriptions();
|
|
979
988
|
const sessionData = await this.createSession();
|
|
980
989
|
if (!sessionData) {
|
|
981
990
|
// Restore the user's message text into the input
|
|
@@ -1044,7 +1053,8 @@ class ChatPanel {
|
|
|
1044
1053
|
if (response.status === 410) {
|
|
1045
1054
|
console.debug('[ChatPanel] Session not resumable (410), creating new session and retrying');
|
|
1046
1055
|
this.currentSessionId = null;
|
|
1047
|
-
this.
|
|
1056
|
+
this._resubscribeChat();
|
|
1057
|
+
this._ensureSubscriptions();
|
|
1048
1058
|
const sessionData = await this.createSession();
|
|
1049
1059
|
if (!sessionData) {
|
|
1050
1060
|
throw new Error('Failed to create replacement session');
|
|
@@ -1061,7 +1071,7 @@ class ChatPanel {
|
|
|
1061
1071
|
const err = await response.json().catch(() => ({}));
|
|
1062
1072
|
throw new Error(err.error || 'Failed to send message');
|
|
1063
1073
|
}
|
|
1064
|
-
console.debug('[ChatPanel] Message accepted, waiting for
|
|
1074
|
+
console.debug('[ChatPanel] Message accepted, waiting for WebSocket events');
|
|
1065
1075
|
} catch (error) {
|
|
1066
1076
|
// Restore pending context so it's not lost
|
|
1067
1077
|
this._pendingContext = savedContext;
|
|
@@ -1914,122 +1924,173 @@ class ChatPanel {
|
|
|
1914
1924
|
}
|
|
1915
1925
|
|
|
1916
1926
|
/**
|
|
1917
|
-
* Ensure
|
|
1918
|
-
*
|
|
1919
|
-
*
|
|
1927
|
+
* Ensure WebSocket subscriptions are established for review and chat topics.
|
|
1928
|
+
* Subscribes to review events (stable for page lifetime) and chat events
|
|
1929
|
+
* (changes when session changes). Subsequent calls are no-ops if already subscribed.
|
|
1920
1930
|
*/
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1931
|
+
_ensureSubscriptions() {
|
|
1932
|
+
window.wsClient.connect();
|
|
1933
|
+
|
|
1934
|
+
// Subscribe to review events (stable for page lifetime)
|
|
1935
|
+
if (this.reviewId && !this._reviewUnsub) {
|
|
1936
|
+
this._reviewUnsub = window.wsClient.subscribe('review:' + this.reviewId, (msg) => {
|
|
1937
|
+
if (msg.type?.startsWith('review:')) {
|
|
1938
|
+
document.dispatchEvent(new CustomEvent(msg.type, {
|
|
1939
|
+
detail: { ...msg }
|
|
1940
|
+
}));
|
|
1941
|
+
}
|
|
1942
|
+
});
|
|
1926
1943
|
}
|
|
1927
1944
|
|
|
1928
|
-
//
|
|
1929
|
-
|
|
1930
|
-
|
|
1945
|
+
// Subscribe to chat session
|
|
1946
|
+
if (this.currentSessionId && !this._chatUnsub) {
|
|
1947
|
+
this._chatUnsub = window.wsClient.subscribe('chat:' + this.currentSessionId, (msg) => {
|
|
1948
|
+
this._handleChatMessage(msg);
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1931
1951
|
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
this.
|
|
1952
|
+
// Listen for WebSocket reconnects — any deltas broadcast during the
|
|
1953
|
+
// reconnect gap are lost, so we re-fetch via HTTP to recover the stream.
|
|
1954
|
+
if (!this._onReconnect) {
|
|
1955
|
+
this._onReconnect = () => { this._recoverAfterReconnect(); };
|
|
1956
|
+
window.addEventListener('wsReconnected', this._onReconnect);
|
|
1957
|
+
}
|
|
1958
|
+
}
|
|
1935
1959
|
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1960
|
+
/**
|
|
1961
|
+
* Recover streaming state after a WebSocket reconnect.
|
|
1962
|
+
* If a stream was in progress when the connection dropped, deltas broadcast
|
|
1963
|
+
* during the gap are lost. Re-fetch the full message history via HTTP and
|
|
1964
|
+
* replace the partial `_streamingContent` with the complete last assistant
|
|
1965
|
+
* message. When not streaming, no action is needed.
|
|
1966
|
+
*/
|
|
1967
|
+
async _recoverAfterReconnect() {
|
|
1968
|
+
if (!this.isStreaming || !this.currentSessionId) return;
|
|
1939
1969
|
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1970
|
+
try {
|
|
1971
|
+
const response = await fetch(`/api/chat/session/${this.currentSessionId}/messages`);
|
|
1972
|
+
if (!response.ok) return;
|
|
1973
|
+
|
|
1974
|
+
const result = await response.json();
|
|
1975
|
+
const messages = result.data?.messages || [];
|
|
1976
|
+
|
|
1977
|
+
// Find the last assistant message — this is the one being streamed
|
|
1978
|
+
let lastAssistant = null;
|
|
1979
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
1980
|
+
if (messages[i].type === 'message' && messages[i].role === 'assistant') {
|
|
1981
|
+
lastAssistant = messages[i];
|
|
1982
|
+
break;
|
|
1944
1983
|
}
|
|
1984
|
+
}
|
|
1945
1985
|
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1986
|
+
if (lastAssistant && lastAssistant.content) {
|
|
1987
|
+
this._streamingContent = lastAssistant.content;
|
|
1988
|
+
// The message is already persisted in the DB, so the stream is
|
|
1989
|
+
// definitively complete. Finalize rather than continuing the
|
|
1990
|
+
// streaming UI (which would leave the Stop button visible, etc.).
|
|
1991
|
+
if (this.isOpen) {
|
|
1992
|
+
this.finalizeStreamingMessage(lastAssistant.id);
|
|
1993
|
+
} else {
|
|
1994
|
+
this.messages.push({ role: 'assistant', content: lastAssistant.content, id: lastAssistant.id });
|
|
1995
|
+
this._finalizeStreaming();
|
|
1952
1996
|
}
|
|
1997
|
+
}
|
|
1998
|
+
} catch (err) {
|
|
1999
|
+
console.warn('[ChatPanel] Failed to recover stream after reconnect:', err);
|
|
2000
|
+
}
|
|
2001
|
+
}
|
|
1953
2002
|
|
|
1954
|
-
|
|
1955
|
-
|
|
2003
|
+
/**
|
|
2004
|
+
* Unsubscribe from the current chat topic and re-subscribe to the new one.
|
|
2005
|
+
* Called whenever `this.currentSessionId` changes.
|
|
2006
|
+
*/
|
|
2007
|
+
_resubscribeChat() {
|
|
2008
|
+
if (this._chatUnsub) { this._chatUnsub(); this._chatUnsub = null; }
|
|
2009
|
+
if (this.currentSessionId) {
|
|
2010
|
+
this._chatUnsub = window.wsClient.subscribe('chat:' + this.currentSessionId, (msg) => {
|
|
2011
|
+
this._handleChatMessage(msg);
|
|
2012
|
+
});
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
1956
2015
|
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
2016
|
+
/**
|
|
2017
|
+
* Handles incoming WebSocket messages for the active chat session.
|
|
2018
|
+
* @param {Object} data - Parsed message object
|
|
2019
|
+
*/
|
|
2020
|
+
_handleChatMessage(data) {
|
|
2021
|
+
try {
|
|
2022
|
+
// Assertion: WebSocket topic scoping guarantees sessionId match.
|
|
2023
|
+
// This warn is a safety net — if it fires, something is wrong upstream.
|
|
2024
|
+
if (data.sessionId !== this.currentSessionId) {
|
|
2025
|
+
console.warn(`[ChatPanel] Unexpected sessionId mismatch: got ${data.sessionId}, expected ${this.currentSessionId}`);
|
|
2026
|
+
return;
|
|
2027
|
+
}
|
|
1960
2028
|
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
switch (data.type) {
|
|
1965
|
-
case 'delta':
|
|
1966
|
-
this._streamingContent += data.text;
|
|
1967
|
-
break;
|
|
1968
|
-
case 'complete':
|
|
1969
|
-
if (this._streamingContent) {
|
|
1970
|
-
this.messages.push({ role: 'assistant', content: this._streamingContent, id: data.messageId });
|
|
1971
|
-
}
|
|
1972
|
-
this._streamingContent = '';
|
|
1973
|
-
this.isStreaming = false;
|
|
1974
|
-
break;
|
|
1975
|
-
case 'error':
|
|
1976
|
-
this._streamingContent = '';
|
|
1977
|
-
this.isStreaming = false;
|
|
1978
|
-
break;
|
|
1979
|
-
// tool_use, status: purely visual, skip when closed
|
|
1980
|
-
}
|
|
1981
|
-
return;
|
|
1982
|
-
}
|
|
2029
|
+
if (data.type !== 'delta') {
|
|
2030
|
+
console.debug('[ChatPanel] WS event:', data.type, 'session:', data.sessionId);
|
|
2031
|
+
}
|
|
1983
2032
|
|
|
2033
|
+
// When the panel is closed, still accumulate internal state
|
|
2034
|
+
// so messages are available when the panel reopens.
|
|
2035
|
+
if (!this.isOpen) {
|
|
1984
2036
|
switch (data.type) {
|
|
1985
2037
|
case 'delta':
|
|
1986
|
-
this._hideThinkingIndicator();
|
|
1987
2038
|
this._streamingContent += data.text;
|
|
1988
|
-
this.updateStreamingMessage(this._streamingContent);
|
|
1989
|
-
break;
|
|
1990
|
-
|
|
1991
|
-
case 'tool_use':
|
|
1992
|
-
this._showToolUse(data.toolName, data.status, data.toolInput);
|
|
1993
2039
|
break;
|
|
1994
|
-
|
|
1995
|
-
case 'status':
|
|
1996
|
-
this._handleAgentStatus(data.status);
|
|
1997
|
-
break;
|
|
1998
|
-
|
|
1999
2040
|
case 'complete':
|
|
2000
|
-
this.
|
|
2041
|
+
if (this._streamingContent) {
|
|
2042
|
+
this.messages.push({ role: 'assistant', content: this._streamingContent, id: data.messageId });
|
|
2043
|
+
}
|
|
2044
|
+
this._streamingContent = '';
|
|
2045
|
+
this.isStreaming = false;
|
|
2001
2046
|
break;
|
|
2002
|
-
|
|
2003
2047
|
case 'error':
|
|
2004
|
-
this.
|
|
2005
|
-
this.
|
|
2048
|
+
this._streamingContent = '';
|
|
2049
|
+
this.isStreaming = false;
|
|
2006
2050
|
break;
|
|
2051
|
+
// tool_use, status: purely visual, skip when closed
|
|
2007
2052
|
}
|
|
2008
|
-
|
|
2009
|
-
console.error('[ChatPanel] SSE parse error:', e);
|
|
2053
|
+
return;
|
|
2010
2054
|
}
|
|
2011
|
-
};
|
|
2012
2055
|
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2056
|
+
switch (data.type) {
|
|
2057
|
+
case 'delta':
|
|
2058
|
+
this._hideThinkingIndicator();
|
|
2059
|
+
this._streamingContent += data.text;
|
|
2060
|
+
this.updateStreamingMessage(this._streamingContent);
|
|
2061
|
+
break;
|
|
2062
|
+
|
|
2063
|
+
case 'tool_use':
|
|
2064
|
+
this._showToolUse(data.toolName, data.status, data.toolInput);
|
|
2065
|
+
break;
|
|
2066
|
+
|
|
2067
|
+
case 'status':
|
|
2068
|
+
this._handleAgentStatus(data.status);
|
|
2069
|
+
break;
|
|
2070
|
+
|
|
2071
|
+
case 'complete':
|
|
2072
|
+
this.finalizeStreamingMessage(data.messageId);
|
|
2073
|
+
break;
|
|
2074
|
+
|
|
2075
|
+
case 'error':
|
|
2076
|
+
this._showError(data.message || 'An error occurred');
|
|
2077
|
+
this._finalizeStreaming();
|
|
2078
|
+
break;
|
|
2020
2079
|
}
|
|
2021
|
-
}
|
|
2080
|
+
} catch (e) {
|
|
2081
|
+
console.error('[ChatPanel] WS parse error:', e);
|
|
2082
|
+
}
|
|
2022
2083
|
}
|
|
2023
2084
|
|
|
2024
2085
|
/**
|
|
2025
|
-
* Close
|
|
2086
|
+
* Close all WebSocket subscriptions (chat and review).
|
|
2026
2087
|
*/
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
this.
|
|
2030
|
-
if (this.
|
|
2031
|
-
this.
|
|
2032
|
-
this.
|
|
2088
|
+
_closeSubscriptions() {
|
|
2089
|
+
if (this._chatUnsub) { this._chatUnsub(); this._chatUnsub = null; }
|
|
2090
|
+
if (this._reviewUnsub) { this._reviewUnsub(); this._reviewUnsub = null; }
|
|
2091
|
+
if (this._onReconnect) {
|
|
2092
|
+
window.removeEventListener('wsReconnected', this._onReconnect);
|
|
2093
|
+
this._onReconnect = null;
|
|
2033
2094
|
}
|
|
2034
2095
|
}
|
|
2035
2096
|
|
|
@@ -2930,7 +2991,7 @@ class ChatPanel {
|
|
|
2930
2991
|
*/
|
|
2931
2992
|
destroy() {
|
|
2932
2993
|
document.removeEventListener('keydown', this._onKeydown);
|
|
2933
|
-
this.
|
|
2994
|
+
this._closeSubscriptions();
|
|
2934
2995
|
this.messages = [];
|
|
2935
2996
|
|
|
2936
2997
|
// Clean up context tooltip
|
|
@@ -2946,6 +3007,9 @@ class ChatPanel {
|
|
|
2946
3007
|
}
|
|
2947
3008
|
}
|
|
2948
3009
|
|
|
3010
|
+
/** Resize configuration for the chat panel, exposed as a static for cross-module use. */
|
|
3011
|
+
ChatPanel.RESIZE_CONFIG = { min: 300, default: 400, cssVar: '--chat-panel-width', storageKey: 'chat-panel-width' };
|
|
3012
|
+
|
|
2949
3013
|
// Make ChatPanel available globally
|
|
2950
3014
|
window.ChatPanel = ChatPanel;
|
|
2951
3015
|
|