llm-interceptor 2.2.0__tar.gz → 2.2.1__tar.gz

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 (70) hide show
  1. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/CHANGELOG.md +10 -0
  2. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/PKG-INFO +1 -1
  3. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/pyproject.toml +1 -1
  4. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/src/cci/__init__.py +1 -1
  5. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/src/cci/cli.py +16 -4
  6. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/README.md +5 -0
  7. llm_interceptor-2.2.1/ui/src/components/layout/SessionsSidebar.tsx +311 -0
  8. llm_interceptor-2.2.0/ui/src/components/layout/SessionsSidebar.tsx +0 -271
  9. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/.github/workflows/ci.yml +0 -0
  10. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/.github/workflows/publish.yml +0 -0
  11. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/.gitignore +0 -0
  12. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/.pre-commit-config.yaml +0 -0
  13. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/README.md +0 -0
  14. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/build_ui.py +0 -0
  15. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/cci-ui-screenshot.png +0 -0
  16. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/cci.example.toml +0 -0
  17. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/lli.example.toml +0 -0
  18. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/src/cci/app.py +0 -0
  19. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/src/cci/config.py +0 -0
  20. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/src/cci/filters.py +0 -0
  21. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/src/cci/logger.py +0 -0
  22. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/src/cci/merger.py +0 -0
  23. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/src/cci/models.py +0 -0
  24. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/src/cci/proxy.py +0 -0
  25. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/src/cci/server.py +0 -0
  26. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/src/cci/splitter.py +0 -0
  27. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/src/cci/storage.py +0 -0
  28. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/src/cci/watch.py +0 -0
  29. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/src/llm_interceptor/__init__.py +0 -0
  30. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/src/llm_interceptor/cli.py +0 -0
  31. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/tests/__init__.py +0 -0
  32. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/tests/test_basic.py +0 -0
  33. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/tests/test_merger.py +0 -0
  34. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/tests/test_watch_cancel.py +0 -0
  35. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/.gitignore +0 -0
  36. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/eslint.config.js +0 -0
  37. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/index.html +0 -0
  38. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/package-lock.json +0 -0
  39. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/package.json +0 -0
  40. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/postcss.config.js +0 -0
  41. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/public/cci-icon.png +0 -0
  42. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/public/vite.svg +0 -0
  43. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/App.css +0 -0
  44. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/App.tsx +0 -0
  45. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/assets/react.svg +0 -0
  46. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/components/chat/ChatBubble.tsx +0 -0
  47. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/components/chat/MessageContent.tsx +0 -0
  48. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/components/chat/ToolCallBlock.tsx +0 -0
  49. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/components/common/CopyButton.tsx +0 -0
  50. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/components/common/JSONViewer.tsx +0 -0
  51. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/components/common/TokenBadge.tsx +0 -0
  52. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/components/common/Tooltip.tsx +0 -0
  53. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/components/layout/EmptyState.tsx +0 -0
  54. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/components/layout/ExchangeDetailsPane.tsx +0 -0
  55. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/components/layout/RequestsPane.tsx +0 -0
  56. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/components/tools/ToolsList.tsx +0 -0
  57. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/hooks/useAnnotations.ts +0 -0
  58. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/hooks/useResizablePanels.ts +0 -0
  59. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/hooks/useSessions.ts +0 -0
  60. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/hooks/useTheme.ts +0 -0
  61. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/index.css +0 -0
  62. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/main.tsx +0 -0
  63. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/types.ts +0 -0
  64. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/utils/ui.ts +0 -0
  65. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/src/utils.ts +0 -0
  66. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/tailwind.config.js +0 -0
  67. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/tsconfig.app.json +0 -0
  68. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/tsconfig.json +0 -0
  69. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/tsconfig.node.json +0 -0
  70. {llm_interceptor-2.2.0 → llm_interceptor-2.2.1}/ui/vite.config.ts +0 -0
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [2.2.1] - 2026-01-06
9
+
10
+ ### Added
11
+
12
+ - **Configurable UI host/port** - Allow configuring the UI host and port
13
+
14
+ ### Fixed
15
+
16
+ - **React hooks usage** - Avoid hooks in sessions map to prevent runtime errors
17
+
8
18
  ## [2.2.0] - 2026-01-05
9
19
 
10
20
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: llm-interceptor
3
- Version: 2.2.0
3
+ Version: 2.2.1
4
4
  Summary: Intercept and analyze LLM traffic from AI coding tools
5
5
  Project-URL: Homepage, https://github.com/chouzz/llm-interceptor
6
6
  Project-URL: Documentation, https://github.com/chouzz/llm-interceptor#readme
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "llm-interceptor"
3
- version = "2.2.0"
3
+ version = "2.2.1"
4
4
  description = "Intercept and analyze LLM traffic from AI coding tools"
5
5
  readme = "README.md"
6
6
  license = { text = "MIT" }
@@ -7,5 +7,5 @@ Note: The import package name `cci` is kept for backward compatibility.
7
7
  New code should prefer `llm_interceptor`.
8
8
  """
9
9
 
10
- __version__ = "2.2.0"
10
+ __version__ = "2.2.1"
11
11
  __author__ = "LLM Interceptor Team"
@@ -400,6 +400,19 @@ def stats(file: str) -> None:
400
400
  default=True,
401
401
  help="Launch the web UI (default: True)",
402
402
  )
403
+ @click.option(
404
+ "--ui-host",
405
+ default="127.0.0.1",
406
+ show_default=True,
407
+ help="Host interface for the web UI (use 0.0.0.0 to expose on the network)",
408
+ )
409
+ @click.option(
410
+ "--ui-port",
411
+ default=8000,
412
+ show_default=True,
413
+ type=int,
414
+ help="Port for the web UI",
415
+ )
403
416
  @click.pass_context
404
417
  def watch(
405
418
  ctx: click.Context,
@@ -408,6 +421,8 @@ def watch(
408
421
  include: tuple[str, ...],
409
422
  debug: bool,
410
423
  ui: bool,
424
+ ui_host: str,
425
+ ui_port: int,
411
426
  ) -> None:
412
427
  """
413
428
  Start watch mode for continuous session capture.
@@ -475,9 +490,6 @@ def watch(
475
490
  if ui:
476
491
  from cci.server import run_server
477
492
 
478
- ui_host = "127.0.0.1"
479
- ui_port = 8000 # TODO: Make configurable
480
-
481
493
  if _is_port_in_use(ui_host, ui_port):
482
494
  ui_url = f"http://{ui_host}:{ui_port}"
483
495
  console.print(
@@ -493,7 +505,7 @@ def watch(
493
505
  server_thread = threading.Thread(
494
506
  target=run_server,
495
507
  args=(watch_manager,),
496
- kwargs={"port": ui_port},
508
+ kwargs={"host": ui_host, "port": ui_port},
497
509
  daemon=True,
498
510
  )
499
511
  server_thread.start()
@@ -23,6 +23,11 @@ This UI is bundled with the `llm-interceptor` Python package.
23
23
  lli watch --include "*example.com*"
24
24
  ```
25
25
 
26
+ To expose the UI on your network:
27
+ ```bash
28
+ lli watch --ui-host 0.0.0.0 --ui-port 8000
29
+ ```
30
+
26
31
  2. **Open the Dashboard**:
27
32
  The CLI will print a URL (usually `http://localhost:8000`). Click it to open the dashboard.
28
33
 
@@ -0,0 +1,311 @@
1
+ import React, { useState, useCallback, useMemo } from 'react';
2
+ import {
3
+ Activity,
4
+ Check,
5
+ ChevronLeft,
6
+ ChevronRight,
7
+ FolderOpen,
8
+ MessageCircle,
9
+ Moon,
10
+ Pencil,
11
+ Sun,
12
+ } from 'lucide-react';
13
+ import type { AnnotationData, SessionSummary } from '../../types';
14
+ import { formatTimestamp } from '../../utils';
15
+ import { Tooltip } from '../common/Tooltip';
16
+
17
+ export const SessionsSidebar: React.FC<{
18
+ width: number;
19
+ isCollapsed: boolean;
20
+ setIsCollapsed: (v: boolean) => void;
21
+ onStartResize: (e: React.MouseEvent) => void;
22
+
23
+ sessionList: SessionSummary[];
24
+ selectedSessionId: string | null;
25
+ onSelectSession: (sessionId: string) => void;
26
+
27
+ isDarkMode: boolean;
28
+ onToggleTheme: () => void;
29
+
30
+ annotations: Record<string, AnnotationData>;
31
+ onUpdateSessionNote: (sessionId: string, note: string) => void;
32
+ }> = ({
33
+ width,
34
+ isCollapsed,
35
+ setIsCollapsed,
36
+ onStartResize,
37
+ sessionList,
38
+ selectedSessionId,
39
+ onSelectSession,
40
+ isDarkMode,
41
+ onToggleTheme,
42
+ annotations,
43
+ onUpdateSessionNote,
44
+ }) => {
45
+ const [editingSessionNote, setEditingSessionNote] = useState<string | null>(null);
46
+
47
+ // Memoize callback functions
48
+ const handleToggleCollapse = useCallback(() => {
49
+ setIsCollapsed(!isCollapsed);
50
+ }, [isCollapsed, setIsCollapsed]);
51
+
52
+ // Memoize rendered sessions
53
+ const renderedSessions = useMemo(() => {
54
+ return sessionList.map((session) => {
55
+ const sessionNote = annotations[session.id]?.session_note || '';
56
+ const hasNote = sessionNote.length > 0;
57
+ const isEditing = editingSessionNote === session.id;
58
+ const isSelected = selectedSessionId === session.id;
59
+
60
+ return (
61
+ <SessionItem
62
+ key={session.id}
63
+ session={session}
64
+ isCollapsed={isCollapsed}
65
+ isSelected={isSelected}
66
+ isEditing={isEditing}
67
+ sessionNote={sessionNote}
68
+ hasNote={hasNote}
69
+ onSelectSession={onSelectSession}
70
+ onSetIsCollapsed={setIsCollapsed}
71
+ onSetEditingSessionNote={setEditingSessionNote}
72
+ onUpdateSessionNote={onUpdateSessionNote}
73
+ />
74
+ );
75
+ });
76
+ }, [
77
+ sessionList,
78
+ annotations,
79
+ editingSessionNote,
80
+ selectedSessionId,
81
+ isCollapsed,
82
+ onSelectSession,
83
+ setIsCollapsed,
84
+ onUpdateSessionNote,
85
+ ]);
86
+
87
+ return (
88
+ <div
89
+ style={{ width: isCollapsed ? '48px' : width }}
90
+ className="flex-shrink-0 border-r border-gray-200 dark:border-slate-800 bg-white dark:bg-[#0b1120] flex flex-col relative transition-all duration-300 ease-in-out"
91
+ >
92
+ {/* Sessions Header */}
93
+ <div
94
+ className={`p-4 border-b border-gray-200 dark:border-slate-800 flex items-center ${
95
+ isCollapsed ? 'justify-center flex-col gap-4' : 'justify-between'
96
+ }`}
97
+ >
98
+ {!isCollapsed && (
99
+ <div className="flex items-center gap-2">
100
+ <Activity size={18} className="text-blue-600 dark:text-blue-500" />
101
+ <h2 className="font-bold text-sm tracking-wide text-slate-700 dark:text-slate-200">
102
+ SESSIONS
103
+ </h2>
104
+ </div>
105
+ )}
106
+
107
+ <div className={`flex ${isCollapsed ? 'flex-col gap-3' : 'gap-1'}`}>
108
+ <button
109
+ onClick={handleToggleCollapse}
110
+ className="p-1.5 hover:bg-gray-100 dark:hover:bg-slate-800 rounded text-slate-500 dark:text-slate-400 transition-colors"
111
+ title={isCollapsed ? 'Expand' : 'Collapse'}
112
+ type="button"
113
+ >
114
+ {isCollapsed ? <ChevronRight size={14} /> : <ChevronLeft size={14} />}
115
+ </button>
116
+ {!isCollapsed && (
117
+ <button
118
+ onClick={onToggleTheme}
119
+ className="p-1.5 hover:bg-gray-100 dark:hover:bg-slate-800 rounded text-slate-500 dark:text-slate-400 transition-colors"
120
+ title="Toggle Theme"
121
+ type="button"
122
+ >
123
+ {isDarkMode ? <Sun size={14} /> : <Moon size={14} />}
124
+ </button>
125
+ )}
126
+ </div>
127
+ </div>
128
+
129
+ {/* Sessions List */}
130
+ <div className="overflow-y-auto flex-1 p-2 space-y-1 custom-scrollbar">
131
+ {renderedSessions}
132
+ </div>
133
+
134
+ {/* Resizer Handle */}
135
+ {!isCollapsed && (
136
+ <div
137
+ className="absolute top-0 right-0 w-1 h-full cursor-col-resize hover:bg-blue-500/50 transition-colors z-10 flex items-center justify-center group"
138
+ onMouseDown={onStartResize}
139
+ >
140
+ <div className="w-[1px] h-full bg-gray-200 dark:bg-slate-800 group-hover:bg-blue-500"></div>
141
+ </div>
142
+ )}
143
+ </div>
144
+ );
145
+ };
146
+
147
+ const SessionItem = React.memo<{
148
+ session: SessionSummary;
149
+ isCollapsed: boolean;
150
+ isSelected: boolean;
151
+ isEditing: boolean;
152
+ sessionNote: string;
153
+ hasNote: boolean;
154
+ onSelectSession: (sessionId: string) => void;
155
+ onSetIsCollapsed: (collapsed: boolean) => void;
156
+ onSetEditingSessionNote: (sessionId: string | null) => void;
157
+ onUpdateSessionNote: (sessionId: string, note: string) => void;
158
+ }>(({
159
+ session,
160
+ isCollapsed,
161
+ isSelected,
162
+ isEditing,
163
+ sessionNote,
164
+ hasNote,
165
+ onSelectSession,
166
+ onSetIsCollapsed,
167
+ onSetEditingSessionNote,
168
+ onUpdateSessionNote,
169
+ }) => {
170
+ const handleSelectSession = useCallback(() => {
171
+ onSelectSession(session.id);
172
+ if (isCollapsed) onSetIsCollapsed(false);
173
+ }, [session.id, onSelectSession, isCollapsed, onSetIsCollapsed]);
174
+
175
+ const handleToggleEdit = useCallback(() => {
176
+ onSetEditingSessionNote(isEditing ? null : session.id);
177
+ }, [isEditing, session.id, onSetEditingSessionNote]);
178
+
179
+ const handleUpdateNote = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
180
+ onUpdateSessionNote(session.id, e.target.value);
181
+ }, [session.id, onUpdateSessionNote]);
182
+
183
+ const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
184
+ if (e.key === 'Escape') {
185
+ onSetEditingSessionNote(null);
186
+ } else if (e.key === 'Enter' && !e.shiftKey) {
187
+ e.preventDefault();
188
+ onSetEditingSessionNote(null);
189
+ }
190
+ }, [onSetEditingSessionNote]);
191
+
192
+ const handleSaveNote = useCallback(() => {
193
+ onSetEditingSessionNote(null);
194
+ }, [onSetEditingSessionNote]);
195
+
196
+ const handleEditNote = useCallback(() => {
197
+ onSetEditingSessionNote(session.id);
198
+ }, [session.id, onSetEditingSessionNote]);
199
+
200
+ return (
201
+ <div>
202
+ {/* Collapsed state */}
203
+ {isCollapsed ? (
204
+ <div
205
+ onClick={handleSelectSession}
206
+ className={`h-12 flex items-center justify-center cursor-pointer border-b border-gray-100 dark:border-slate-800/50 relative rounded-lg mb-1 ${
207
+ isSelected ? 'bg-blue-50 dark:bg-slate-800' : ''
208
+ }`}
209
+ >
210
+ <FolderOpen
211
+ size={20}
212
+ className={
213
+ isSelected
214
+ ? 'text-blue-600 dark:text-blue-400'
215
+ : 'text-slate-400 dark:text-slate-600'
216
+ }
217
+ />
218
+ {hasNote && <div className="absolute top-1 right-1 w-2 h-2 bg-amber-500 rounded-full"></div>}
219
+ </div>
220
+ ) : (
221
+ <div>
222
+ <div
223
+ onClick={handleSelectSession}
224
+ className={`px-3 py-3 border-b border-gray-100 dark:border-slate-800/50 cursor-pointer transition-colors group relative rounded-lg mb-1 ${
225
+ isSelected ? 'bg-blue-50 dark:bg-slate-800/80 shadow-md z-10' : 'hover:bg-gray-50 dark:hover:bg-slate-800/30'
226
+ }`}
227
+ title={session.id}
228
+ >
229
+ {/* Action Buttons (appear on hover) */}
230
+ <div className="absolute right-2 top-2 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity z-20">
231
+ <button
232
+ onClick={(e) => { e.stopPropagation(); handleToggleEdit(); }}
233
+ className="p-1.5 bg-white dark:bg-slate-700 rounded shadow-sm hover:scale-110"
234
+ title="Edit note"
235
+ type="button"
236
+ >
237
+ <Pencil size={12} className="text-slate-500 dark:text-slate-300" />
238
+ </button>
239
+ </div>
240
+
241
+ {/* Selection indicator */}
242
+ {isSelected && (
243
+ <div className="absolute left-0 top-3 bottom-3 w-1 bg-blue-500 rounded-r-full"></div>
244
+ )}
245
+
246
+ <div className="flex items-center justify-between mb-1">
247
+ <div className="flex items-center gap-2">
248
+ <FolderOpen
249
+ size={16}
250
+ className={
251
+ isSelected
252
+ ? 'text-blue-600 dark:text-blue-400'
253
+ : 'text-slate-400 dark:text-slate-600'
254
+ }
255
+ />
256
+ <span className="font-semibold truncate max-w-[120px] text-slate-700 dark:text-slate-200" title={session.id}>
257
+ {session.id.replace('session_', '')}
258
+ </span>
259
+ </div>
260
+ <div className="flex items-center gap-1 text-xs text-slate-400 dark:text-slate-500">
261
+ <span>{session.request_count}</span>
262
+ {hasNote && !isEditing && <MessageCircle size={10} className="text-amber-500" />}
263
+ </div>
264
+ </div>
265
+ <div className="text-xs text-slate-400 dark:text-slate-500">
266
+ {formatTimestamp(session.timestamp)}
267
+ </div>
268
+
269
+ {/* Note Section - inside the card */}
270
+ {!isEditing && hasNote && (
271
+ <Tooltip text={sessionNote}>
272
+ <div
273
+ className="mt-2 px-2 py-1 text-[10px] text-amber-700 dark:text-amber-300 bg-amber-50 dark:bg-amber-950/20 rounded border-l-2 border-amber-400 dark:border-amber-600 cursor-pointer hover:bg-amber-100 dark:hover:bg-amber-950/30 transition-colors truncate"
274
+ onClick={(e) => { e.stopPropagation(); handleEditNote(); }}
275
+ title="Click to edit"
276
+ >
277
+ {sessionNote}
278
+ </div>
279
+ </Tooltip>
280
+ )}
281
+
282
+ {/* Note Editor - inside the card */}
283
+ {isEditing && (
284
+ <div className="mt-2 relative">
285
+ <textarea
286
+ autoFocus
287
+ value={sessionNote}
288
+ onChange={handleUpdateNote}
289
+ onKeyDown={handleKeyDown}
290
+ onBlur={handleSaveNote}
291
+ placeholder="Add a note..."
292
+ className="w-full text-xs p-1.5 pr-6 border border-amber-300 dark:border-amber-700 rounded bg-amber-50 dark:bg-amber-950/30 text-slate-700 dark:text-slate-200 placeholder-slate-400 dark:placeholder-slate-500 resize-none focus:outline-none focus:ring-1 focus:ring-amber-400 dark:focus:ring-amber-600"
293
+ rows={2}
294
+ onClick={(e) => e.stopPropagation()}
295
+ />
296
+ <button
297
+ onClick={(e) => { e.stopPropagation(); handleSaveNote(); }}
298
+ className="absolute top-1 right-1 p-0.5 hover:bg-amber-200 dark:hover:bg-amber-800 rounded text-amber-600 dark:text-amber-400"
299
+ title="Done (Enter)"
300
+ type="button"
301
+ >
302
+ <Check size={10} />
303
+ </button>
304
+ </div>
305
+ )}
306
+ </div>
307
+ </div>
308
+ )}
309
+ </div>
310
+ );
311
+ });
@@ -1,271 +0,0 @@
1
- import React, { useState, useCallback, useMemo } from 'react';
2
- import {
3
- Activity,
4
- Check,
5
- ChevronLeft,
6
- ChevronRight,
7
- FolderOpen,
8
- MessageCircle,
9
- Moon,
10
- Pencil,
11
- Sun,
12
- } from 'lucide-react';
13
- import type { AnnotationData, SessionSummary } from '../../types';
14
- import { formatTimestamp } from '../../utils';
15
- import { Tooltip } from '../common/Tooltip';
16
-
17
- export const SessionsSidebar: React.FC<{
18
- width: number;
19
- isCollapsed: boolean;
20
- setIsCollapsed: (v: boolean) => void;
21
- onStartResize: (e: React.MouseEvent) => void;
22
-
23
- sessionList: SessionSummary[];
24
- selectedSessionId: string | null;
25
- onSelectSession: (sessionId: string) => void;
26
-
27
- isDarkMode: boolean;
28
- onToggleTheme: () => void;
29
-
30
- annotations: Record<string, AnnotationData>;
31
- onUpdateSessionNote: (sessionId: string, note: string) => void;
32
- }> = ({
33
- width,
34
- isCollapsed,
35
- setIsCollapsed,
36
- onStartResize,
37
- sessionList,
38
- selectedSessionId,
39
- onSelectSession,
40
- isDarkMode,
41
- onToggleTheme,
42
- annotations,
43
- onUpdateSessionNote,
44
- }) => {
45
- const [editingSessionNote, setEditingSessionNote] = useState<string | null>(null);
46
-
47
- // Memoize callback functions
48
- const handleToggleCollapse = useCallback(() => {
49
- setIsCollapsed(!isCollapsed);
50
- }, [isCollapsed, setIsCollapsed]);
51
-
52
- // Memoize rendered sessions
53
- const renderedSessions = useMemo(() => {
54
- return sessionList.map((session) => {
55
- const sessionNote = annotations[session.id]?.session_note || '';
56
- const hasNote = sessionNote.length > 0;
57
- const isEditing = editingSessionNote === session.id;
58
- const isSelected = selectedSessionId === session.id;
59
-
60
- const handleSelectSession = useCallback(() => {
61
- onSelectSession(session.id);
62
- if (isCollapsed) setIsCollapsed(false);
63
- }, [session.id, onSelectSession, isCollapsed, setIsCollapsed]);
64
-
65
- const handleToggleEdit = useCallback(() => {
66
- setEditingSessionNote(isEditing ? null : session.id);
67
- }, [isEditing, session.id]);
68
-
69
- const handleUpdateNote = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
70
- onUpdateSessionNote(session.id, e.target.value);
71
- }, [session.id, onUpdateSessionNote]);
72
-
73
- const handleKeyDown = useCallback((e: React.KeyboardEvent<HTMLTextAreaElement>) => {
74
- if (e.key === 'Escape') {
75
- setEditingSessionNote(null);
76
- } else if (e.key === 'Enter' && !e.shiftKey) {
77
- e.preventDefault();
78
- setEditingSessionNote(null);
79
- }
80
- }, []);
81
-
82
- const handleSaveNote = useCallback(() => {
83
- setEditingSessionNote(null);
84
- }, []);
85
-
86
- const handleEditNote = useCallback(() => {
87
- setEditingSessionNote(session.id);
88
- }, [session.id]);
89
-
90
- return (
91
- <div key={session.id}>
92
- {/* Collapsed state */}
93
- {isCollapsed ? (
94
- <div
95
- onClick={handleSelectSession}
96
- className={`h-12 flex items-center justify-center cursor-pointer border-b border-gray-100 dark:border-slate-800/50 relative rounded-lg mb-1 ${
97
- isSelected ? 'bg-blue-50 dark:bg-slate-800' : ''
98
- }`}
99
- >
100
- <FolderOpen
101
- size={20}
102
- className={
103
- isSelected
104
- ? 'text-blue-600 dark:text-blue-400'
105
- : 'text-slate-400 dark:text-slate-600'
106
- }
107
- />
108
- {hasNote && <div className="absolute top-1 right-1 w-2 h-2 bg-amber-500 rounded-full"></div>}
109
- </div>
110
- ) : (
111
- <div>
112
- <div
113
- onClick={handleSelectSession}
114
- className={`px-3 py-3 border-b border-gray-100 dark:border-slate-800/50 cursor-pointer transition-colors group relative rounded-lg mb-1 ${
115
- isSelected ? 'bg-blue-50 dark:bg-slate-800/80 shadow-md z-10' : 'hover:bg-gray-50 dark:hover:bg-slate-800/30'
116
- }`}
117
- title={session.id}
118
- >
119
- {/* Action Buttons (appear on hover) */}
120
- <div className="absolute right-2 top-2 flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity z-20">
121
- <button
122
- onClick={(e) => { e.stopPropagation(); handleToggleEdit(); }}
123
- className="p-1.5 bg-white dark:bg-slate-700 rounded shadow-sm hover:scale-110"
124
- title="Edit note"
125
- type="button"
126
- >
127
- <Pencil size={12} className="text-slate-500 dark:text-slate-300" />
128
- </button>
129
- </div>
130
-
131
- {/* Selection indicator */}
132
- {isSelected && (
133
- <div className="absolute left-0 top-3 bottom-3 w-1 bg-blue-500 rounded-r-full"></div>
134
- )}
135
-
136
- <div className="flex items-center justify-between mb-1">
137
- <div className="flex items-center gap-2">
138
- <FolderOpen
139
- size={16}
140
- className={
141
- isSelected
142
- ? 'text-blue-600 dark:text-blue-400'
143
- : 'text-slate-400 dark:text-slate-600'
144
- }
145
- />
146
- <span className="font-semibold truncate max-w-[120px] text-slate-700 dark:text-slate-200" title={session.id}>
147
- {session.id.replace('session_', '')}
148
- </span>
149
- </div>
150
- <div className="flex items-center gap-1 text-xs text-slate-400 dark:text-slate-500">
151
- <span>{session.request_count}</span>
152
- {hasNote && !isEditing && <MessageCircle size={10} className="text-amber-500" />}
153
- </div>
154
- </div>
155
- <div className="text-xs text-slate-400 dark:text-slate-500">
156
- {formatTimestamp(session.timestamp)}
157
- </div>
158
-
159
- {/* Note Section - inside the card */}
160
- {!isEditing && hasNote && (
161
- <Tooltip text={sessionNote}>
162
- <div
163
- className="mt-2 px-2 py-1 text-[10px] text-amber-700 dark:text-amber-300 bg-amber-50 dark:bg-amber-950/20 rounded border-l-2 border-amber-400 dark:border-amber-600 cursor-pointer hover:bg-amber-100 dark:hover:bg-amber-950/30 transition-colors truncate"
164
- onClick={(e) => { e.stopPropagation(); handleEditNote(); }}
165
- title="Click to edit"
166
- >
167
- {sessionNote}
168
- </div>
169
- </Tooltip>
170
- )}
171
-
172
- {/* Note Editor - inside the card */}
173
- {isEditing && (
174
- <div className="mt-2 relative">
175
- <textarea
176
- autoFocus
177
- value={sessionNote}
178
- onChange={handleUpdateNote}
179
- onKeyDown={handleKeyDown}
180
- onBlur={handleSaveNote}
181
- placeholder="Add a note..."
182
- className="w-full text-xs p-1.5 pr-6 border border-amber-300 dark:border-amber-700 rounded bg-amber-50 dark:bg-amber-950/30 text-slate-700 dark:text-slate-200 placeholder-slate-400 dark:placeholder-slate-500 resize-none focus:outline-none focus:ring-1 focus:ring-amber-400 dark:focus:ring-amber-600"
183
- rows={2}
184
- onClick={(e) => e.stopPropagation()}
185
- />
186
- <button
187
- onClick={(e) => { e.stopPropagation(); handleSaveNote(); }}
188
- className="absolute top-1 right-1 p-0.5 hover:bg-amber-200 dark:hover:bg-amber-800 rounded text-amber-600 dark:text-amber-400"
189
- title="Done (Enter)"
190
- type="button"
191
- >
192
- <Check size={10} />
193
- </button>
194
- </div>
195
- )}
196
- </div>
197
- </div>
198
- )}
199
- </div>
200
- );
201
- });
202
- }, [
203
- sessionList,
204
- annotations,
205
- editingSessionNote,
206
- selectedSessionId,
207
- isCollapsed,
208
- onSelectSession,
209
- setIsCollapsed,
210
- onUpdateSessionNote,
211
- ]);
212
-
213
- return (
214
- <div
215
- style={{ width: isCollapsed ? '48px' : width }}
216
- className="flex-shrink-0 border-r border-gray-200 dark:border-slate-800 bg-white dark:bg-[#0b1120] flex flex-col relative transition-all duration-300 ease-in-out"
217
- >
218
- {/* Sessions Header */}
219
- <div
220
- className={`p-4 border-b border-gray-200 dark:border-slate-800 flex items-center ${
221
- isCollapsed ? 'justify-center flex-col gap-4' : 'justify-between'
222
- }`}
223
- >
224
- {!isCollapsed && (
225
- <div className="flex items-center gap-2">
226
- <Activity size={18} className="text-blue-600 dark:text-blue-500" />
227
- <h2 className="font-bold text-sm tracking-wide text-slate-700 dark:text-slate-200">
228
- SESSIONS
229
- </h2>
230
- </div>
231
- )}
232
-
233
- <div className={`flex ${isCollapsed ? 'flex-col gap-3' : 'gap-1'}`}>
234
- <button
235
- onClick={handleToggleCollapse}
236
- className="p-1.5 hover:bg-gray-100 dark:hover:bg-slate-800 rounded text-slate-500 dark:text-slate-400 transition-colors"
237
- title={isCollapsed ? 'Expand' : 'Collapse'}
238
- type="button"
239
- >
240
- {isCollapsed ? <ChevronRight size={14} /> : <ChevronLeft size={14} />}
241
- </button>
242
- {!isCollapsed && (
243
- <button
244
- onClick={onToggleTheme}
245
- className="p-1.5 hover:bg-gray-100 dark:hover:bg-slate-800 rounded text-slate-500 dark:text-slate-400 transition-colors"
246
- title="Toggle Theme"
247
- type="button"
248
- >
249
- {isDarkMode ? <Sun size={14} /> : <Moon size={14} />}
250
- </button>
251
- )}
252
- </div>
253
- </div>
254
-
255
- {/* Sessions List */}
256
- <div className="overflow-y-auto flex-1 p-2 space-y-1 custom-scrollbar">
257
- {renderedSessions}
258
- </div>
259
-
260
- {/* Resizer Handle */}
261
- {!isCollapsed && (
262
- <div
263
- className="absolute top-0 right-0 w-1 h-full cursor-col-resize hover:bg-blue-500/50 transition-colors z-10 flex items-center justify-center group"
264
- onMouseDown={onStartResize}
265
- >
266
- <div className="w-[1px] h-full bg-gray-200 dark:bg-slate-800 group-hover:bg-blue-500"></div>
267
- </div>
268
- )}
269
- </div>
270
- );
271
- };