hdsp-jupyter-extension 2.0.7__py3-none-any.whl → 2.0.10__py3-none-any.whl

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 (84) hide show
  1. agent_server/core/embedding_service.py +67 -46
  2. agent_server/core/rag_manager.py +40 -17
  3. agent_server/core/retriever.py +12 -6
  4. agent_server/core/vllm_embedding_service.py +246 -0
  5. agent_server/langchain/ARCHITECTURE.md +7 -51
  6. agent_server/langchain/agent.py +39 -20
  7. agent_server/langchain/custom_middleware.py +206 -62
  8. agent_server/langchain/hitl_config.py +6 -9
  9. agent_server/langchain/llm_factory.py +85 -1
  10. agent_server/langchain/logging_utils.py +52 -13
  11. agent_server/langchain/prompts.py +85 -45
  12. agent_server/langchain/tools/__init__.py +14 -10
  13. agent_server/langchain/tools/file_tools.py +266 -40
  14. agent_server/langchain/tools/file_utils.py +334 -0
  15. agent_server/langchain/tools/jupyter_tools.py +0 -1
  16. agent_server/langchain/tools/lsp_tools.py +264 -0
  17. agent_server/langchain/tools/resource_tools.py +12 -12
  18. agent_server/langchain/tools/search_tools.py +3 -158
  19. agent_server/main.py +7 -0
  20. agent_server/routers/langchain_agent.py +207 -102
  21. agent_server/routers/rag.py +8 -3
  22. hdsp_agent_core/models/rag.py +15 -1
  23. hdsp_agent_core/services/rag_service.py +6 -1
  24. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/build_log.json +1 -1
  25. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/package.json +3 -2
  26. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.4770ec0fb2d173b6deb4.js → hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.2d9fb488c82498c45c2d.js +251 -5
  27. hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.2d9fb488c82498c45c2d.js.map +1 -0
  28. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.29cf4312af19e86f82af.js → hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.dc6434bee96ab03a0539.js +1831 -274
  29. hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.dc6434bee96ab03a0539.js.map +1 -0
  30. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.61343eb4cf0577e74b50.js → hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.4a252df3ade74efee8d6.js +11 -9
  31. hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.4a252df3ade74efee8d6.js.map +1 -0
  32. jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js → hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js +2 -209
  33. hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js.map +1 -0
  34. jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js → hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js +209 -2
  35. hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js.map +1 -0
  36. jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js → hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js +212 -3
  37. hdsp_jupyter_extension-2.0.10.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js.map +1 -0
  38. {hdsp_jupyter_extension-2.0.7.dist-info → hdsp_jupyter_extension-2.0.10.dist-info}/METADATA +1 -3
  39. hdsp_jupyter_extension-2.0.10.dist-info/RECORD +144 -0
  40. jupyter_ext/__init__.py +18 -0
  41. jupyter_ext/_version.py +1 -1
  42. jupyter_ext/handlers.py +176 -1
  43. jupyter_ext/labextension/build_log.json +1 -1
  44. jupyter_ext/labextension/package.json +3 -2
  45. jupyter_ext/labextension/static/{frontend_styles_index_js.4770ec0fb2d173b6deb4.js → frontend_styles_index_js.2d9fb488c82498c45c2d.js} +251 -5
  46. jupyter_ext/labextension/static/frontend_styles_index_js.2d9fb488c82498c45c2d.js.map +1 -0
  47. jupyter_ext/labextension/static/{lib_index_js.29cf4312af19e86f82af.js → lib_index_js.dc6434bee96ab03a0539.js} +1831 -274
  48. jupyter_ext/labextension/static/lib_index_js.dc6434bee96ab03a0539.js.map +1 -0
  49. jupyter_ext/labextension/static/{remoteEntry.61343eb4cf0577e74b50.js → remoteEntry.4a252df3ade74efee8d6.js} +11 -9
  50. jupyter_ext/labextension/static/remoteEntry.4a252df3ade74efee8d6.js.map +1 -0
  51. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js → jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js +2 -209
  52. jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js.map +1 -0
  53. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js → jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js +209 -2
  54. jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js.map +1 -0
  55. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js → jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js +212 -3
  56. jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js.map +1 -0
  57. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.4770ec0fb2d173b6deb4.js.map +0 -1
  58. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.29cf4312af19e86f82af.js.map +0 -1
  59. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.61343eb4cf0577e74b50.js.map +0 -1
  60. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js.map +0 -1
  61. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js.map +0 -1
  62. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js.map +0 -1
  63. hdsp_jupyter_extension-2.0.7.dist-info/RECORD +0 -141
  64. jupyter_ext/labextension/static/frontend_styles_index_js.4770ec0fb2d173b6deb4.js.map +0 -1
  65. jupyter_ext/labextension/static/lib_index_js.29cf4312af19e86f82af.js.map +0 -1
  66. jupyter_ext/labextension/static/remoteEntry.61343eb4cf0577e74b50.js.map +0 -1
  67. jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js.map +0 -1
  68. jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js.map +0 -1
  69. jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js.map +0 -1
  70. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/etc/jupyter/jupyter_server_config.d/hdsp_jupyter_extension.json +0 -0
  71. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/install.json +0 -0
  72. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b80.c095373419d05e6f141a.js +0 -0
  73. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b80.c095373419d05e6f141a.js.map +0 -0
  74. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b81.61e75fb98ecff46cf836.js +0 -0
  75. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/static/node_modules_emotion_use-insertion-effect-with-fallbacks_dist_emotion-use-insertion-effect-wi-3ba6b81.61e75fb98ecff46cf836.js.map +0 -0
  76. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/static/style.js +0 -0
  77. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_babel_runtime_helpers_esm_extends_js-node_modules_emotion_serialize_dist-051195.e2553aab0c3963b83dd7.js +0 -0
  78. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_babel_runtime_helpers_esm_extends_js-node_modules_emotion_serialize_dist-051195.e2553aab0c3963b83dd7.js.map +0 -0
  79. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_styled_dist_emotion-styled_browser_development_esm_js.661fb5836f4978a7c6e1.js +0 -0
  80. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_styled_dist_emotion-styled_browser_development_esm_js.661fb5836f4978a7c6e1.js.map +0 -0
  81. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js +0 -0
  82. {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.10.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js.map +0 -0
  83. {hdsp_jupyter_extension-2.0.7.dist-info → hdsp_jupyter_extension-2.0.10.dist-info}/WHEEL +0 -0
  84. {hdsp_jupyter_extension-2.0.7.dist-info → hdsp_jupyter_extension-2.0.10.dist-info}/licenses/LICENSE +0 -0
@@ -1088,99 +1088,125 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
1088
1088
  try {
1089
1089
  // Use LLM prompt if available, otherwise use the display content
1090
1090
  const messageToSend = llmPrompt || displayContentText;
1091
- console.log('[AgentPanel] Sending message with agentThreadId:', agentThreadId);
1092
- await apiService.sendMessageStream({
1093
- message: messageToSend,
1094
- conversationId: conversationId || undefined,
1095
- llmConfig: currentConfig // Include API keys with request
1096
- },
1097
- // onChunk callback - update message content incrementally
1098
- (chunk) => {
1099
- streamedContent += chunk;
1100
- setMessages(prev => prev.map(msg => msg.id === assistantMessageId && isChatMessage(msg)
1101
- ? { ...msg, content: streamedContent }
1102
- : msg));
1103
- },
1104
- // onMetadata callback - update conversationId and metadata
1105
- (metadata) => {
1106
- if (metadata.conversationId && !conversationId) {
1107
- setConversationId(metadata.conversationId);
1108
- }
1109
- if (metadata.provider || metadata.model) {
1091
+ console.log('[AgentPanel] Sending message with mode:', inputMode, 'agentThreadId:', agentThreadId);
1092
+ // Chat 모드: 단순 Q&A (sendChatStream)
1093
+ // Agent V2 모드 또는 그 외: LangChain Deep Agent (sendAgentV2Stream)
1094
+ if (inputMode === 'chat') {
1095
+ // 단순 Chat 모드 - /chat/stream 사용
1096
+ await apiService.sendChatStream({
1097
+ message: messageToSend,
1098
+ conversationId: conversationId || undefined,
1099
+ llmConfig: currentConfig
1100
+ },
1101
+ // onChunk callback
1102
+ (chunk) => {
1103
+ streamedContent += chunk;
1110
1104
  setMessages(prev => prev.map(msg => msg.id === assistantMessageId && isChatMessage(msg)
1111
- ? {
1112
- ...msg,
1113
- metadata: {
1114
- ...msg.metadata,
1115
- provider: metadata.provider,
1116
- model: metadata.model
1105
+ ? { ...msg, content: streamedContent }
1106
+ : msg));
1107
+ },
1108
+ // onMetadata callback
1109
+ (metadata) => {
1110
+ if (metadata.conversationId && !conversationId) {
1111
+ setConversationId(metadata.conversationId);
1112
+ }
1113
+ });
1114
+ }
1115
+ else {
1116
+ // Agent V2 모드 - /agent/langchain/stream 사용 (HITL, Todo, 도구 실행)
1117
+ await apiService.sendAgentV2Stream({
1118
+ message: messageToSend,
1119
+ conversationId: conversationId || undefined,
1120
+ llmConfig: currentConfig // Include API keys with request
1121
+ },
1122
+ // onChunk callback - update message content incrementally
1123
+ (chunk) => {
1124
+ streamedContent += chunk;
1125
+ setMessages(prev => prev.map(msg => msg.id === assistantMessageId && isChatMessage(msg)
1126
+ ? { ...msg, content: streamedContent }
1127
+ : msg));
1128
+ },
1129
+ // onMetadata callback - update conversationId and metadata
1130
+ (metadata) => {
1131
+ if (metadata.conversationId && !conversationId) {
1132
+ setConversationId(metadata.conversationId);
1133
+ }
1134
+ if (metadata.provider || metadata.model) {
1135
+ setMessages(prev => prev.map(msg => msg.id === assistantMessageId && isChatMessage(msg)
1136
+ ? {
1137
+ ...msg,
1138
+ metadata: {
1139
+ ...msg.metadata,
1140
+ provider: metadata.provider,
1141
+ model: metadata.model
1142
+ }
1117
1143
  }
1144
+ : msg));
1145
+ }
1146
+ },
1147
+ // onDebug callback - show debug status in gray
1148
+ (status) => {
1149
+ setDebugStatus(status);
1150
+ },
1151
+ // onInterrupt callback - show approval dialog
1152
+ (interrupt) => {
1153
+ approvalPendingRef.current = true;
1154
+ // Capture threadId from interrupt for context persistence
1155
+ if (interrupt.threadId && !agentThreadId) {
1156
+ setAgentThreadId(interrupt.threadId);
1157
+ console.log('[AgentPanel] Captured agentThreadId from interrupt:', interrupt.threadId);
1158
+ }
1159
+ // Auto-approve search/file/resource tools - execute immediately without user interaction
1160
+ if (interrupt.action === 'search_notebook_cells_tool'
1161
+ || interrupt.action === 'check_resource_tool'
1162
+ || interrupt.action === 'read_file_tool') {
1163
+ void handleAutoToolInterrupt(interrupt);
1164
+ return;
1165
+ }
1166
+ if (autoApproveEnabled) {
1167
+ // 자동 승인 모드일 때도 인터럽트 메시지를 생성하여 코드블럭으로 표시
1168
+ upsertInterruptMessage(interrupt, true);
1169
+ void resumeFromInterrupt(interrupt, 'approve');
1170
+ return;
1171
+ }
1172
+ if (interrupt.action === 'jupyter_cell_tool' && interrupt.args?.code) {
1173
+ const shouldQueue = shouldExecuteInNotebook(interrupt.args.code);
1174
+ if (isAutoApprovedCode(interrupt.args.code)) {
1175
+ if (shouldQueue) {
1176
+ queueApprovalCell(interrupt.args.code);
1177
+ }
1178
+ void resumeFromInterrupt(interrupt, 'approve');
1179
+ return;
1118
1180
  }
1119
- : msg));
1120
- }
1121
- },
1122
- // onDebug callback - show debug status in gray
1123
- (status) => {
1124
- setDebugStatus(status);
1125
- },
1126
- // onInterrupt callback - show approval dialog
1127
- (interrupt) => {
1128
- approvalPendingRef.current = true;
1129
- // Capture threadId from interrupt for context persistence
1130
- if (interrupt.threadId && !agentThreadId) {
1131
- setAgentThreadId(interrupt.threadId);
1132
- console.log('[AgentPanel] Captured agentThreadId from interrupt:', interrupt.threadId);
1133
- }
1134
- // Auto-approve search/file/resource tools - execute immediately without user interaction
1135
- if (interrupt.action === 'search_workspace_tool'
1136
- || interrupt.action === 'search_notebook_cells_tool'
1137
- || interrupt.action === 'check_resource_tool'
1138
- || interrupt.action === 'list_files_tool'
1139
- || interrupt.action === 'read_file_tool') {
1140
- void handleAutoToolInterrupt(interrupt);
1141
- return;
1142
- }
1143
- if (autoApproveEnabled) {
1144
- void resumeFromInterrupt(interrupt, 'approve');
1145
- return;
1146
- }
1147
- if (interrupt.action === 'jupyter_cell_tool' && interrupt.args?.code) {
1148
- const shouldQueue = shouldExecuteInNotebook(interrupt.args.code);
1149
- if (isAutoApprovedCode(interrupt.args.code)) {
1150
1181
  if (shouldQueue) {
1151
1182
  queueApprovalCell(interrupt.args.code);
1152
1183
  }
1153
- void resumeFromInterrupt(interrupt, 'approve');
1154
- return;
1155
1184
  }
1156
- if (shouldQueue) {
1157
- queueApprovalCell(interrupt.args.code);
1185
+ setInterruptData(interrupt);
1186
+ upsertInterruptMessage(interrupt);
1187
+ setIsLoading(false);
1188
+ setIsStreaming(false);
1189
+ },
1190
+ // onTodos callback - update todo list UI
1191
+ (newTodos) => {
1192
+ setTodos(newTodos);
1193
+ },
1194
+ // onDebugClear callback - clear debug status
1195
+ () => {
1196
+ setDebugStatus(null);
1197
+ },
1198
+ // onToolCall callback - add cells to notebook
1199
+ handleToolCall,
1200
+ // onComplete callback - capture thread_id for context persistence
1201
+ (data) => {
1202
+ if (data.threadId) {
1203
+ setAgentThreadId(data.threadId);
1204
+ console.log('[AgentPanel] Captured agentThreadId for context persistence:', data.threadId);
1158
1205
  }
1159
- }
1160
- setInterruptData(interrupt);
1161
- upsertInterruptMessage(interrupt);
1162
- setIsLoading(false);
1163
- setIsStreaming(false);
1164
- },
1165
- // onTodos callback - update todo list UI
1166
- (newTodos) => {
1167
- setTodos(newTodos);
1168
- },
1169
- // onDebugClear callback - clear debug status
1170
- () => {
1171
- setDebugStatus(null);
1172
- },
1173
- // onToolCall callback - add cells to notebook
1174
- handleToolCall,
1175
- // onComplete callback - capture thread_id for context persistence
1176
- (data) => {
1177
- if (data.threadId) {
1178
- setAgentThreadId(data.threadId);
1179
- console.log('[AgentPanel] Captured agentThreadId for context persistence:', data.threadId);
1180
- }
1181
- },
1182
- // threadId - pass existing thread_id to continue context
1183
- agentThreadId || undefined);
1206
+ },
1207
+ // threadId - pass existing thread_id to continue context
1208
+ agentThreadId || undefined);
1209
+ }
1184
1210
  }
1185
1211
  catch (error) {
1186
1212
  const message = error instanceof Error ? error.message : 'Failed to send message';
@@ -2013,7 +2039,7 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
2013
2039
  execution_method: 'subprocess'
2014
2040
  };
2015
2041
  };
2016
- const upsertInterruptMessage = (interrupt) => {
2042
+ const upsertInterruptMessage = (interrupt, autoApproved) => {
2017
2043
  const interruptMessageId = interruptMessageIdRef.current || makeMessageId('interrupt');
2018
2044
  interruptMessageIdRef.current = interruptMessageId;
2019
2045
  const interruptMessage = {
@@ -2021,7 +2047,14 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
2021
2047
  role: 'system',
2022
2048
  content: interrupt.description || '코드 실행 승인이 필요합니다.',
2023
2049
  timestamp: Date.now(),
2024
- metadata: { interrupt }
2050
+ metadata: {
2051
+ interrupt: {
2052
+ ...interrupt,
2053
+ resolved: autoApproved || false,
2054
+ decision: autoApproved ? 'approve' : undefined,
2055
+ autoApproved: autoApproved || false
2056
+ }
2057
+ }
2025
2058
  };
2026
2059
  setMessages(prev => {
2027
2060
  const hasExisting = prev.some(msg => msg.id === interruptMessageId);
@@ -2177,8 +2210,12 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
2177
2210
  if (!pathCheck.valid) {
2178
2211
  return { success: false, error: pathCheck.error };
2179
2212
  }
2180
- const maxLines = typeof params.maxLines === 'number' ? params.maxLines : 1000;
2181
- const safeMaxLines = Math.max(0, maxLines);
2213
+ // Support both old (maxLines) and new (offset/limit) parameters
2214
+ const offset = typeof params.offset === 'number' ? Math.max(0, params.offset) : 0;
2215
+ const limit = typeof params.limit === 'number'
2216
+ ? params.limit
2217
+ : (typeof params.maxLines === 'number' ? params.maxLines : 500);
2218
+ const safeLimit = Math.max(0, limit);
2182
2219
  const contentsResult = await fetchContentsModel(params.path, { content: true, format: 'text' });
2183
2220
  if (!contentsResult.success) {
2184
2221
  return { success: false, error: contentsResult.error };
@@ -2198,13 +2235,18 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
2198
2235
  content = JSON.stringify(content, null, 2);
2199
2236
  }
2200
2237
  const lines = content.split('\n');
2201
- const sliced = lines.slice(0, safeMaxLines);
2238
+ const totalLines = lines.length;
2239
+ // Apply offset and limit
2240
+ const sliced = lines.slice(offset, offset + safeLimit);
2202
2241
  return {
2203
2242
  success: true,
2204
2243
  output: sliced.join('\n'),
2205
2244
  metadata: {
2206
2245
  lineCount: sliced.length,
2207
- truncated: lines.length > safeMaxLines
2246
+ totalLines,
2247
+ offset,
2248
+ limit: safeLimit,
2249
+ truncated: totalLines > offset + safeLimit
2208
2250
  }
2209
2251
  };
2210
2252
  };
@@ -2214,18 +2256,7 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
2214
2256
  console.log('[AgentPanel] Auto-approving tool:', action, args);
2215
2257
  try {
2216
2258
  let executionResult;
2217
- if (action === 'search_workspace_tool') {
2218
- setDebugStatus(`🔍 검색 실행 중: ${args?.pattern || ''}`);
2219
- executionResult = await apiService.searchWorkspace({
2220
- pattern: args?.pattern || '',
2221
- file_types: args?.file_types || ['*.py', '*.ipynb'],
2222
- path: args?.path || '.',
2223
- max_results: args?.max_results || 50,
2224
- case_sensitive: args?.case_sensitive || false
2225
- });
2226
- console.log('[AgentPanel] search_workspace result:', executionResult);
2227
- }
2228
- else if (action === 'search_notebook_cells_tool') {
2259
+ if (action === 'search_notebook_cells_tool') {
2229
2260
  setDebugStatus(`🔍 노트북 검색 실행 중: ${args?.pattern || ''}`);
2230
2261
  executionResult = await apiService.searchNotebookCells({
2231
2262
  pattern: args?.pattern || '',
@@ -2237,7 +2268,7 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
2237
2268
  console.log('[AgentPanel] search_notebook_cells result:', executionResult);
2238
2269
  }
2239
2270
  else if (action === 'check_resource_tool') {
2240
- const filesList = Array.isArray(args?.files) ? args.files : (args?.files ? [args.files] : []);
2271
+ const filesList = args?.files || [];
2241
2272
  setDebugStatus(`📊 리소스 체크 중: ${filesList.join(', ') || 'system'}`);
2242
2273
  executionResult = await apiService.checkResource({
2243
2274
  files: filesList,
@@ -2247,22 +2278,13 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
2247
2278
  });
2248
2279
  console.log('[AgentPanel] check_resource result:', executionResult);
2249
2280
  }
2250
- else if (action === 'list_files_tool') {
2251
- setDebugStatus('📂 파일 목록 조회 중...');
2252
- const listParams = {
2253
- path: typeof args?.path === 'string' ? args.path : '.',
2254
- recursive: args?.recursive ?? false,
2255
- pattern: args?.pattern ?? undefined
2256
- };
2257
- executionResult = await executeListFilesTool(listParams);
2258
- console.log('[AgentPanel] list_files result:', executionResult);
2259
- }
2260
2281
  else if (action === 'read_file_tool') {
2261
2282
  setDebugStatus('📄 파일 읽는 중...');
2262
2283
  const readParams = {
2263
2284
  path: typeof args?.path === 'string' ? args.path : '',
2264
2285
  encoding: typeof args?.encoding === 'string' ? args.encoding : undefined,
2265
- maxLines: args?.max_lines ?? args?.maxLines
2286
+ offset: typeof args?.offset === 'number' ? args.offset : 0,
2287
+ limit: typeof args?.limit === 'number' ? args.limit : (args?.max_lines ?? args?.maxLines ?? 500)
2266
2288
  };
2267
2289
  executionResult = await executeReadFileTool(readParams);
2268
2290
  console.log('[AgentPanel] read_file result:', executionResult);
@@ -2306,15 +2328,15 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
2306
2328
  approvalPendingRef.current = true;
2307
2329
  const autoApproveEnabled = getAutoApproveEnabled(llmConfig || (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_4__.getLLMConfig)() || (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_4__.getDefaultLLMConfig)());
2308
2330
  // Handle next interrupt (could be another search or code execution)
2309
- if (nextInterrupt.action === 'search_workspace_tool'
2310
- || nextInterrupt.action === 'search_notebook_cells_tool'
2331
+ if (nextInterrupt.action === 'search_notebook_cells_tool'
2311
2332
  || nextInterrupt.action === 'check_resource_tool'
2312
- || nextInterrupt.action === 'list_files_tool'
2313
2333
  || nextInterrupt.action === 'read_file_tool') {
2314
2334
  void handleAutoToolInterrupt(nextInterrupt);
2315
2335
  return;
2316
2336
  }
2317
2337
  if (autoApproveEnabled) {
2338
+ // 자동 승인 모드일 때도 인터럽트 메시지를 생성하여 코드블럭으로 표시
2339
+ upsertInterruptMessage(nextInterrupt, true);
2318
2340
  void resumeFromInterrupt(nextInterrupt, 'approve');
2319
2341
  return;
2320
2342
  }
@@ -2390,6 +2412,276 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
2390
2412
  };
2391
2413
  }
2392
2414
  }
2415
+ else if (interrupt.action === 'edit_file_tool') {
2416
+ // Handle edit_file_tool - read file, perform replacement, write back
2417
+ try {
2418
+ setDebugStatus('✏️ 파일 수정 중...');
2419
+ const filePath = interrupt.args?.path || '';
2420
+ const oldString = interrupt.args?.old_string || '';
2421
+ const newString = interrupt.args?.new_string || '';
2422
+ const replaceAll = interrupt.args?.replace_all || false;
2423
+ // Read current file content
2424
+ const readResult = await apiService.readFile(filePath);
2425
+ if (!readResult.success || readResult.content === undefined) {
2426
+ throw new Error(readResult.error || 'Failed to read file');
2427
+ }
2428
+ const originalContent = readResult.content;
2429
+ const occurrences = (originalContent.match(new RegExp(oldString.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length;
2430
+ if (occurrences === 0) {
2431
+ throw new Error(`String not found in file: '${oldString.slice(0, 100)}...'`);
2432
+ }
2433
+ if (occurrences > 1 && !replaceAll) {
2434
+ throw new Error(`String appears ${occurrences} times. Use replace_all=True or provide more context.`);
2435
+ }
2436
+ // Perform replacement
2437
+ const newContent = replaceAll
2438
+ ? originalContent.split(oldString).join(newString)
2439
+ : originalContent.replace(oldString, newString);
2440
+ // Generate diff for logging
2441
+ const diffLines = [];
2442
+ const originalLines = originalContent.split('\n');
2443
+ const newLines = newContent.split('\n');
2444
+ diffLines.push(`--- ${filePath} (before)`);
2445
+ diffLines.push(`+++ ${filePath} (after)`);
2446
+ // Simple diff generation
2447
+ let i = 0, j = 0;
2448
+ while (i < originalLines.length || j < newLines.length) {
2449
+ if (i < originalLines.length && j < newLines.length && originalLines[i] === newLines[j]) {
2450
+ i++;
2451
+ j++;
2452
+ }
2453
+ else if (i < originalLines.length && (j >= newLines.length || originalLines[i] !== newLines[j])) {
2454
+ diffLines.push(`-${originalLines[i]}`);
2455
+ i++;
2456
+ }
2457
+ else {
2458
+ diffLines.push(`+${newLines[j]}`);
2459
+ j++;
2460
+ }
2461
+ }
2462
+ const diff = diffLines.join('\n');
2463
+ // Write the modified content
2464
+ const writeResult = await apiService.writeFile(filePath, newContent, {
2465
+ encoding: 'utf-8',
2466
+ overwrite: true
2467
+ });
2468
+ console.log('[AgentPanel] edit_file result:', writeResult);
2469
+ resumeDecision = 'edit';
2470
+ resumeArgs = {
2471
+ ...interrupt.args,
2472
+ execution_result: {
2473
+ ...writeResult,
2474
+ diff,
2475
+ occurrences,
2476
+ lines_added: newLines.length - originalLines.length > 0 ? newLines.length - originalLines.length : 0,
2477
+ lines_removed: originalLines.length - newLines.length > 0 ? originalLines.length - newLines.length : 0,
2478
+ }
2479
+ };
2480
+ }
2481
+ catch (error) {
2482
+ const message = error instanceof Error ? error.message : 'File edit failed';
2483
+ console.error('[AgentPanel] edit_file error:', error);
2484
+ resumeDecision = 'edit';
2485
+ resumeArgs = {
2486
+ ...interrupt.args,
2487
+ execution_result: { success: false, error: message }
2488
+ };
2489
+ }
2490
+ }
2491
+ else if (interrupt.action === 'multiedit_file_tool') {
2492
+ // Handle multiedit_file_tool - apply multiple edits atomically (Crush 패턴)
2493
+ try {
2494
+ setDebugStatus('✏️ 다중 파일 수정 중...');
2495
+ const filePath = interrupt.args?.path || '';
2496
+ const edits = interrupt.args?.edits || [];
2497
+ if (!filePath || edits.length === 0) {
2498
+ throw new Error('File path and edits are required');
2499
+ }
2500
+ // Read current file content
2501
+ const readResult = await apiService.readFile(filePath);
2502
+ if (!readResult.success || readResult.content === undefined) {
2503
+ throw new Error(readResult.error || 'Failed to read file');
2504
+ }
2505
+ const originalContent = readResult.content;
2506
+ let currentContent = originalContent;
2507
+ let editsApplied = 0;
2508
+ let editsFailed = 0;
2509
+ const failedEdits = [];
2510
+ // Apply edits sequentially
2511
+ for (let i = 0; i < edits.length; i++) {
2512
+ const edit = edits[i];
2513
+ const oldString = edit.old_string || '';
2514
+ const newString = edit.new_string || '';
2515
+ const replaceAll = edit.replace_all || false;
2516
+ const occurrences = (currentContent.match(new RegExp(oldString.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length;
2517
+ if (occurrences === 0) {
2518
+ failedEdits.push(`Edit ${i + 1}: String not found`);
2519
+ editsFailed++;
2520
+ continue;
2521
+ }
2522
+ if (occurrences > 1 && !replaceAll) {
2523
+ failedEdits.push(`Edit ${i + 1}: String appears ${occurrences} times (use replace_all)`);
2524
+ editsFailed++;
2525
+ continue;
2526
+ }
2527
+ // Apply this edit
2528
+ currentContent = replaceAll
2529
+ ? currentContent.split(oldString).join(newString)
2530
+ : currentContent.replace(oldString, newString);
2531
+ editsApplied++;
2532
+ }
2533
+ // Generate diff
2534
+ const diffLines = [];
2535
+ const originalLines = originalContent.split('\n');
2536
+ const newLines = currentContent.split('\n');
2537
+ diffLines.push(`--- ${filePath} (before)`);
2538
+ diffLines.push(`+++ ${filePath} (after)`);
2539
+ let i = 0, j = 0;
2540
+ while (i < originalLines.length || j < newLines.length) {
2541
+ if (i < originalLines.length && j < newLines.length && originalLines[i] === newLines[j]) {
2542
+ i++;
2543
+ j++;
2544
+ }
2545
+ else if (i < originalLines.length && (j >= newLines.length || originalLines[i] !== newLines[j])) {
2546
+ diffLines.push(`-${originalLines[i]}`);
2547
+ i++;
2548
+ }
2549
+ else {
2550
+ diffLines.push(`+${newLines[j]}`);
2551
+ j++;
2552
+ }
2553
+ }
2554
+ const diff = diffLines.join('\n');
2555
+ // Write the modified content if any edits succeeded
2556
+ if (editsApplied > 0) {
2557
+ const writeResult = await apiService.writeFile(filePath, currentContent, {
2558
+ encoding: 'utf-8',
2559
+ overwrite: true
2560
+ });
2561
+ console.log('[AgentPanel] multiedit_file result:', writeResult, 'applied:', editsApplied, 'failed:', editsFailed);
2562
+ resumeDecision = 'edit';
2563
+ resumeArgs = {
2564
+ ...interrupt.args,
2565
+ execution_result: {
2566
+ ...writeResult,
2567
+ diff,
2568
+ edits_applied: editsApplied,
2569
+ edits_failed: editsFailed,
2570
+ failed_details: failedEdits,
2571
+ lines_added: newLines.length - originalLines.length > 0 ? newLines.length - originalLines.length : 0,
2572
+ lines_removed: originalLines.length - newLines.length > 0 ? originalLines.length - newLines.length : 0,
2573
+ }
2574
+ };
2575
+ }
2576
+ else {
2577
+ throw new Error(`All ${edits.length} edits failed: ${failedEdits.join('; ')}`);
2578
+ }
2579
+ }
2580
+ catch (error) {
2581
+ const message = error instanceof Error ? error.message : 'Multi-edit failed';
2582
+ console.error('[AgentPanel] multiedit_file error:', error);
2583
+ resumeDecision = 'edit';
2584
+ resumeArgs = {
2585
+ ...interrupt.args,
2586
+ execution_result: { success: false, error: message, edits_applied: 0, edits_failed: 0 }
2587
+ };
2588
+ }
2589
+ }
2590
+ else if (interrupt.action === 'diagnostics_tool') {
2591
+ // Handle diagnostics_tool - get LSP diagnostics via LSP Bridge
2592
+ try {
2593
+ setDebugStatus('🔍 LSP 진단 조회 중...');
2594
+ const filePath = interrupt.args?.path || null;
2595
+ const severityFilter = interrupt.args?.severity_filter || null;
2596
+ // Get LSP Bridge instance
2597
+ const lspBridge = window._hdspLSPBridge;
2598
+ if (lspBridge && lspBridge.isLSPAvailable()) {
2599
+ const result = await lspBridge.getDiagnostics(filePath);
2600
+ // Apply severity filter if specified
2601
+ let diagnostics = result.diagnostics;
2602
+ if (severityFilter) {
2603
+ diagnostics = diagnostics.filter((d) => d.severity === severityFilter);
2604
+ }
2605
+ resumeDecision = 'approve';
2606
+ resumeArgs = {
2607
+ ...interrupt.args,
2608
+ execution_result: {
2609
+ success: true,
2610
+ lsp_available: true,
2611
+ diagnostics
2612
+ }
2613
+ };
2614
+ }
2615
+ else {
2616
+ // LSP not available - return empty result
2617
+ console.log('[AgentPanel] LSP not available, returning empty diagnostics');
2618
+ resumeDecision = 'approve';
2619
+ resumeArgs = {
2620
+ ...interrupt.args,
2621
+ execution_result: {
2622
+ success: true,
2623
+ lsp_available: false,
2624
+ diagnostics: []
2625
+ }
2626
+ };
2627
+ }
2628
+ }
2629
+ catch (error) {
2630
+ const message = error instanceof Error ? error.message : 'Diagnostics failed';
2631
+ console.error('[AgentPanel] diagnostics_tool error:', error);
2632
+ resumeDecision = 'approve';
2633
+ resumeArgs = {
2634
+ ...interrupt.args,
2635
+ execution_result: { success: false, lsp_available: false, error: message, diagnostics: [] }
2636
+ };
2637
+ }
2638
+ }
2639
+ else if (interrupt.action === 'references_tool') {
2640
+ // Handle references_tool - find symbol references
2641
+ try {
2642
+ setDebugStatus('🔗 참조 검색 중...');
2643
+ const symbol = interrupt.args?.symbol || '';
2644
+ const filePath = interrupt.args?.path || null;
2645
+ // Get LSP Bridge instance
2646
+ const lspBridge = window._hdspLSPBridge;
2647
+ if (lspBridge && lspBridge.isLSPAvailable()) {
2648
+ const result = await lspBridge.getReferences(symbol, filePath, interrupt.args?.line, interrupt.args?.character);
2649
+ resumeDecision = 'approve';
2650
+ resumeArgs = {
2651
+ ...interrupt.args,
2652
+ execution_result: {
2653
+ success: result.success,
2654
+ lsp_available: true,
2655
+ locations: result.locations,
2656
+ used_grep: false
2657
+ }
2658
+ };
2659
+ }
2660
+ else {
2661
+ // LSP not available - suggest using execute_command_tool with grep
2662
+ console.log('[AgentPanel] LSP not available for references, suggesting grep fallback');
2663
+ resumeDecision = 'approve';
2664
+ resumeArgs = {
2665
+ ...interrupt.args,
2666
+ execution_result: {
2667
+ success: false,
2668
+ lsp_available: false,
2669
+ locations: [],
2670
+ used_grep: false
2671
+ }
2672
+ };
2673
+ }
2674
+ }
2675
+ catch (error) {
2676
+ const message = error instanceof Error ? error.message : 'References search failed';
2677
+ console.error('[AgentPanel] references_tool error:', error);
2678
+ resumeDecision = 'approve';
2679
+ resumeArgs = {
2680
+ ...interrupt.args,
2681
+ execution_result: { success: false, lsp_available: false, error: message, locations: [] }
2682
+ };
2683
+ }
2684
+ }
2393
2685
  else if (interrupt.action === 'execute_command_tool') {
2394
2686
  const command = (interrupt.args?.command || '').trim();
2395
2687
  // Default stdin to "y\n" for interactive prompts (yes/no)
@@ -2532,15 +2824,15 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
2532
2824
  approvalPendingRef.current = true;
2533
2825
  const autoApproveEnabled = getAutoApproveEnabled(llmConfig || (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_4__.getLLMConfig)() || (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_4__.getDefaultLLMConfig)());
2534
2826
  // Auto-approve search/file/resource tools
2535
- if (nextInterrupt.action === 'search_workspace_tool'
2536
- || nextInterrupt.action === 'search_notebook_cells_tool'
2827
+ if (nextInterrupt.action === 'search_notebook_cells_tool'
2537
2828
  || nextInterrupt.action === 'check_resource_tool'
2538
- || nextInterrupt.action === 'list_files_tool'
2539
2829
  || nextInterrupt.action === 'read_file_tool') {
2540
2830
  void handleAutoToolInterrupt(nextInterrupt);
2541
2831
  return;
2542
2832
  }
2543
2833
  if (autoApproveEnabled) {
2834
+ // 자동 승인 모드일 때도 인터럽트 메시지를 생성하여 코드블럭으로 표시
2835
+ upsertInterruptMessage(nextInterrupt, true);
2544
2836
  void resumeFromInterrupt(nextInterrupt, 'approve');
2545
2837
  return;
2546
2838
  }
@@ -2633,26 +2925,24 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
2633
2925
  }
2634
2926
  // 노트북이 없는 경우
2635
2927
  if (!notebook) {
2636
- // Python 에러가 감지되면 파일 수정 모드로 전환
2637
- if (detectPythonError(currentInput)) {
2638
- console.log('[AgentPanel] Agent mode: No notebook, but Python error detected - attempting file fix');
2639
- // Python 에러 수정 처리 시도
2640
- const handled = await handlePythonErrorFix(currentInput);
2641
- if (handled) {
2642
- // 파일 수정 모드로 처리됨
2643
- const userMessage = {
2644
- id: makeMessageId(),
2645
- role: 'user',
2646
- content: currentInput,
2647
- timestamp: Date.now(),
2648
- };
2649
- setMessages(prev => [...prev, userMessage]);
2650
- setInput('');
2651
- return;
2652
- }
2653
- // 파일 경로를 찾지 못함 - 아래 로직으로 계속 진행
2654
- console.log('[AgentPanel] Agent mode: No file path found, continuing...');
2655
- }
2928
+ // [DISABLED] Python 에러가 감지되면 파일 수정 모드로 전환
2929
+ // 레거시 기능은 /file/action API가 구현되지 않아 비활성화됨
2930
+ // if (detectPythonError(currentInput)) {
2931
+ // console.log('[AgentPanel] Agent mode: No notebook, but Python error detected - attempting file fix');
2932
+ // const handled = await handlePythonErrorFix(currentInput);
2933
+ // if (handled) {
2934
+ // const userMessage: IChatMessage = {
2935
+ // id: makeMessageId(),
2936
+ // role: 'user',
2937
+ // content: currentInput,
2938
+ // timestamp: Date.now(),
2939
+ // };
2940
+ // setMessages(prev => [...prev, userMessage]);
2941
+ // setInput('');
2942
+ // return;
2943
+ // }
2944
+ // console.log('[AgentPanel] Agent mode: No file path found, continuing...');
2945
+ // }
2656
2946
  // 파일 수정 관련 자연어 요청 감지 (에러, 고쳐, 수정, fix 등)
2657
2947
  const fileFixRequestPatterns = [
2658
2948
  /에러.*해결/i,
@@ -2745,29 +3035,25 @@ SyntaxError: '(' was never closed
2745
3035
  return;
2746
3036
  }
2747
3037
  }
2748
- // Python 에러 감지 및 파일 수정 모드 (Chat 모드에서만)
2749
- // 파일 경로를 추출할 있는 경우에만 파일 수정 모드 사용
2750
- // 그렇지 않으면 일반 LLM 스트림으로 처리
2751
- if (inputMode === 'chat' && detectPythonError(currentInput)) {
2752
- console.log('[AgentPanel] Python error detected in message, attempting file fix...');
2753
- // Python 에러 수정 처리 시도 (파일 경로를 찾을 수 있는 경우에만 처리됨)
2754
- const handled = await handlePythonErrorFix(currentInput);
2755
- if (handled) {
2756
- // 파일 수정 모드로 처리됨 - 사용자 메시지 추가 후 종료
2757
- const userMessage = {
2758
- id: makeMessageId(),
2759
- role: 'user',
2760
- content: currentInput,
2761
- timestamp: Date.now(),
2762
- };
2763
- setMessages(prev => [...prev, userMessage]);
2764
- setInput('');
2765
- return;
2766
- }
2767
- // 파일 경로를 찾지 못해 handled=false 반환됨
2768
- // 일반 스트림으로 폴백 (아래 로직에서 사용자 메시지 추가됨)
2769
- console.log('[AgentPanel] No file path found in error, using regular LLM stream');
2770
- }
3038
+ // [DISABLED] Python 에러 감지 및 파일 수정 모드 (Chat 모드에서만)
3039
+ // 레거시 기능은 /file/action API가 구현되지 않아 비활성화됨
3040
+ // LangChain agent의 edit_file_tool을 사용하도록 변경됨
3041
+ // if (inputMode === 'chat' && detectPythonError(currentInput)) {
3042
+ // console.log('[AgentPanel] Python error detected in message, attempting file fix...');
3043
+ // const handled = await handlePythonErrorFix(currentInput);
3044
+ // if (handled) {
3045
+ // const userMessage: IChatMessage = {
3046
+ // id: makeMessageId(),
3047
+ // role: 'user',
3048
+ // content: currentInput,
3049
+ // timestamp: Date.now(),
3050
+ // };
3051
+ // setMessages(prev => [...prev, userMessage]);
3052
+ // setInput('');
3053
+ // return;
3054
+ // }
3055
+ // console.log('[AgentPanel] No file path found in error, using regular LLM stream');
3056
+ // }
2771
3057
  // Use the display prompt (input) for the user message, or use a fallback if input is empty
2772
3058
  const displayContent = currentInput || (llmPrompt ? '셀 분석 요청' : '');
2773
3059
  await sendChatMessage({
@@ -2803,26 +3089,26 @@ SyntaxError: '(' was never closed
2803
3089
  e.preventDefault();
2804
3090
  handleSendMessage();
2805
3091
  }
2806
- // Shift+Tab: 모드 전환 (chat agent)
3092
+ // Shift+Tab: 모드 전환 (chat agent → agent_v2 → chat 순환)
2807
3093
  if (e.key === 'Tab' && e.shiftKey) {
2808
3094
  e.preventDefault();
2809
- setInputMode(prev => prev === 'chat' ? 'agent' : 'chat');
3095
+ setInputMode(prev => prev === 'chat' ? 'agent' : prev === 'agent' ? 'agent_v2' : 'chat');
2810
3096
  return;
2811
3097
  }
2812
3098
  // Cmd/Ctrl + . : 모드 전환 (대체 단축키)
2813
3099
  if (e.key === '.' && (e.metaKey || e.ctrlKey)) {
2814
3100
  e.preventDefault();
2815
- setInputMode(prev => prev === 'chat' ? 'agent' : 'chat');
3101
+ setInputMode(prev => prev === 'chat' ? 'agent' : prev === 'agent' ? 'agent_v2' : 'chat');
2816
3102
  }
2817
3103
  // Tab (without Shift): Agent 모드일 때 드롭다운 토글
2818
- if (e.key === 'Tab' && !e.shiftKey && inputMode === 'agent') {
3104
+ if (e.key === 'Tab' && !e.shiftKey && inputMode !== 'chat') {
2819
3105
  e.preventDefault();
2820
3106
  setShowModeDropdown(prev => !prev);
2821
3107
  }
2822
3108
  };
2823
- // 모드 토글 함수
3109
+ // 모드 토글 함수 (chat → agent → agent_v2 → chat 순환)
2824
3110
  const toggleMode = () => {
2825
- setInputMode(prev => prev === 'chat' ? 'agent' : 'chat');
3111
+ setInputMode(prev => prev === 'chat' ? 'agent' : prev === 'agent' ? 'agent_v2' : 'chat');
2826
3112
  setShowModeDropdown(false);
2827
3113
  };
2828
3114
  const clearChat = () => {
@@ -2965,19 +3251,25 @@ SyntaxError: '(' was never closed
2965
3251
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("p", null, "\uC548\uB155\uD558\uC138\uC694! HDSP Agent\uC785\uB2C8\uB2E4."),
2966
3252
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("p", { className: "jp-agent-empty-hint" }, inputMode === 'agent'
2967
3253
  ? '노트북 작업을 자연어로 요청하세요. 예: "데이터 시각화 해줘"'
2968
- : '메시지를 입력하거나 아래 버튼으로 Agent 모드를 선택하세요.'))) : (messages.map(msg => {
3254
+ : inputMode === 'agent_v2'
3255
+ ? 'Deep Agent 모드입니다. HITL 승인을 통해 도구 실행을 제어할 수 있습니다.'
3256
+ : '메시지를 입력하거나 아래 버튼으로 Agent 모드를 선택하세요.'))) : (messages.map(msg => {
2969
3257
  if (isChatMessage(msg)) {
2970
3258
  // 일반 Chat 메시지
2971
3259
  const isAssistant = msg.role === 'assistant';
2972
3260
  const isShellOutput = msg.metadata?.kind === 'shell-output';
2973
3261
  const interruptAction = msg.metadata?.interrupt?.action;
2974
3262
  const isWriteFile = interruptAction === 'write_file_tool';
3263
+ const isEditFile = interruptAction === 'edit_file_tool';
2975
3264
  const writePath = (isWriteFile
2976
3265
  && typeof msg.metadata?.interrupt?.args?.path === 'string') ? msg.metadata?.interrupt?.args?.path : '';
3266
+ const editPath = (isEditFile
3267
+ && typeof msg.metadata?.interrupt?.args?.path === 'string') ? msg.metadata?.interrupt?.args?.path : '';
3268
+ const autoApproved = msg.metadata?.interrupt?.autoApproved;
2977
3269
  const headerRole = msg.role === 'user'
2978
3270
  ? '사용자'
2979
3271
  : msg.role === 'system'
2980
- ? (isShellOutput ? 'shell 실행' : '승인 요청')
3272
+ ? (isShellOutput ? 'shell 실행' : (autoApproved ? '자동 승인됨' : '승인 요청'))
2981
3273
  : 'Agent';
2982
3274
  return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { key: msg.id, className: isAssistant
2983
3275
  ? 'jp-agent-message jp-agent-message-assistant-inline'
@@ -2985,21 +3277,43 @@ SyntaxError: '(' was never closed
2985
3277
  !isAssistant && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-message-header" },
2986
3278
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-message-role" }, headerRole),
2987
3279
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-message-time" }, new Date(msg.timestamp).toLocaleTimeString()))),
2988
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: `jp-agent-message-content${streamingMessageId === msg.id ? ' streaming' : ''}${isShellOutput ? ' jp-agent-message-content-shell' : ''}` }, msg.role === 'system' && msg.metadata?.interrupt ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-interrupt-inline" },
2989
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-interrupt-description" }, msg.content),
3280
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: `jp-agent-message-content${streamingMessageId === msg.id ? ' streaming' : ''}${isShellOutput ? ' jp-agent-message-content-shell' : ''}` }, msg.role === 'system' && msg.metadata?.interrupt ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: `jp-agent-interrupt-inline${msg.metadata?.interrupt?.autoApproved ? ' jp-agent-interrupt-auto-approved' : ''}` },
3281
+ !msg.metadata?.interrupt?.autoApproved && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-interrupt-description" }, msg.content)),
2990
3282
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-interrupt-action" },
2991
3283
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-interrupt-action-args" }, (() => {
2992
3284
  const command = msg.metadata?.interrupt?.args?.command;
2993
3285
  const code = msg.metadata?.interrupt?.args?.code || msg.metadata?.interrupt?.args?.content || '';
2994
- const snippet = (command || code || '(no details)');
2995
- const language = command ? 'bash' : 'python';
3286
+ // Handle edit_file_tool with diff preview
3287
+ let snippet;
3288
+ let language;
3289
+ if (isEditFile) {
3290
+ const oldStr = msg.metadata?.interrupt?.args?.old_string || '';
3291
+ const newStr = msg.metadata?.interrupt?.args?.new_string || '';
3292
+ const replaceAll = msg.metadata?.interrupt?.args?.replace_all;
3293
+ // Generate simple diff preview
3294
+ const oldPreview = oldStr.length > 500 ? oldStr.slice(0, 500) + '...' : oldStr;
3295
+ const newPreview = newStr.length > 500 ? newStr.slice(0, 500) + '...' : newStr;
3296
+ snippet = `--- ${editPath} (before)\n+++ ${editPath} (after)\n` +
3297
+ oldPreview.split('\n').map((line) => `-${line}`).join('\n') + '\n' +
3298
+ newPreview.split('\n').map((line) => `+${line}`).join('\n') +
3299
+ (replaceAll ? '\n\n(replace_all: true)' : '');
3300
+ language = 'diff';
3301
+ }
3302
+ else {
3303
+ snippet = (command || code || '(no details)');
3304
+ language = command ? 'bash' : 'python';
3305
+ }
2996
3306
  const resolved = msg.metadata?.interrupt?.resolved;
2997
3307
  const decision = msg.metadata?.interrupt?.decision;
2998
- const resolvedText = decision === 'reject' ? '거부됨' : '승인됨';
2999
- const resolvedClass = decision === 'reject' ? 'jp-agent-interrupt-actions--rejected' : 'jp-agent-interrupt-actions--resolved';
3000
- const actionHtml = resolved
3308
+ const autoApproved = msg.metadata?.interrupt?.autoApproved;
3309
+ // 자동 승인일 때는 코드블럭 헤더에 배지가 표시되므로 actionHtml에는 표시하지 않음
3310
+ const resolvedText = autoApproved ? '' : (decision === 'reject' ? '거부됨' : '승인됨');
3311
+ const resolvedClass = autoApproved ? '' : (decision === 'reject' ? 'jp-agent-interrupt-actions--rejected' : 'jp-agent-interrupt-actions--resolved');
3312
+ const actionHtml = resolved && !autoApproved
3001
3313
  ? `<div class="jp-agent-interrupt-actions ${resolvedClass}">${resolvedText}</div>`
3002
- : `
3314
+ : resolved && autoApproved
3315
+ ? '' // 자동 승인일 때는 actionHtml 비움 (코드블럭 헤더에 배지 표시)
3316
+ : `
3003
3317
  <div class="code-block-actions jp-agent-interrupt-actions">
3004
3318
  <button class="jp-agent-interrupt-approve-btn" data-action="approve">승인</button>
3005
3319
  <button class="jp-agent-interrupt-reject-btn" data-action="reject">거부</button>
@@ -3011,7 +3325,12 @@ SyntaxError: '(' was never closed
3011
3325
  const safePath = escapeHtml(writePath);
3012
3326
  html = html.replace(/<span class="code-block-language">[^<]*<\/span>/, `<span class="code-block-language jp-agent-interrupt-path">${safePath}</span>`);
3013
3327
  }
3014
- return html.replace('</div>', `${actionHtml}</div>`);
3328
+ if (isEditFile && editPath) {
3329
+ const safePath = escapeHtml(editPath);
3330
+ html = html.replace(/<span class="code-block-language">[^<]*<\/span>/, `<span class="code-block-language jp-agent-interrupt-path">✏️ ${safePath}</span>`);
3331
+ }
3332
+ // actionHtml이 비어있지 않을 때만 추가
3333
+ return actionHtml ? html.replace('</div>', `${actionHtml}</div>`) : html;
3015
3334
  })();
3016
3335
  return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-RenderedHTMLCommon", style: { padding: '0 4px' }, dangerouslySetInnerHTML: { __html: renderedHtml }, onClick: (event) => {
3017
3336
  const target = event.target;
@@ -3073,7 +3392,7 @@ SyntaxError: '(' was never closed
3073
3392
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", { className: "jp-agent-file-fix-apply", onClick: () => applyFileFix(fix), title: `${fix.path} 파일에 수정 적용` }, "\uC801\uC6A9\uD558\uAE30"))))),
3074
3393
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", { className: "jp-agent-file-fixes-dismiss", onClick: () => setPendingFileFixes([]), title: "\uC218\uC815 \uC81C\uC548 \uB2EB\uAE30" }, "\uB2EB\uAE30"))),
3075
3394
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { ref: messagesEndRef })),
3076
- todos.length > 0 && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-todo-compact" },
3395
+ inputMode !== 'chat' && todos.length > 0 && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-todo-compact" },
3077
3396
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-todo-compact-header", onClick: () => setIsTodoExpanded(!isTodoExpanded) },
3078
3397
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-todo-compact-left" },
3079
3398
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("svg", { className: `jp-agent-todo-expand-icon ${isTodoExpanded ? 'jp-agent-todo-expand-icon--expanded' : ''}`, viewBox: "0 0 16 16", fill: "currentColor", width: "12", height: "12" },
@@ -3117,21 +3436,25 @@ SyntaxError: '(' was never closed
3117
3436
  !statusText.startsWith('오류:') && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-debug-ellipsis", "aria-hidden": "true" }))))),
3118
3437
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-input-container" },
3119
3438
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-input-wrapper" },
3120
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement("textarea", { className: `jp-agent-input ${inputMode === 'agent' ? 'jp-agent-input--agent-mode' : ''} ${isRejectionMode ? 'jp-agent-input--rejection-mode' : ''}`, value: input, onChange: (e) => setInput(e.target.value), onKeyDown: handleKeyDown, placeholder: isRejectionMode
3439
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("textarea", { className: `jp-agent-input ${inputMode !== 'chat' ? 'jp-agent-input--agent-mode' : ''} ${isRejectionMode ? 'jp-agent-input--rejection-mode' : ''}`, value: input, onChange: (e) => setInput(e.target.value), onKeyDown: handleKeyDown, placeholder: isRejectionMode
3121
3440
  ? '다른 방향 제시'
3122
3441
  : (inputMode === 'agent'
3123
3442
  ? '노트북 작업을 입력하세요... (예: 데이터 시각화 해줘)'
3124
- : '메시지를 입력하세요...'), rows: 3, disabled: isLoading || isAgentRunning }),
3443
+ : inputMode === 'agent_v2'
3444
+ ? 'Deep Agent 요청을 입력하세요... (HITL 승인 모드)'
3445
+ : '메시지를 입력하세요...'), rows: 3, disabled: isLoading || isAgentRunning }),
3125
3446
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", { className: "jp-agent-send-button", onClick: handleSendMessage, disabled: (!input.trim() && !isRejectionMode) || isLoading || isStreaming || isAgentRunning, title: isRejectionMode ? "거부 전송 (Enter)" : "전송 (Enter)" }, isAgentRunning ? '실행 중...' : (isRejectionMode ? '거부' : '전송'))),
3126
3447
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-mode-bar" },
3127
3448
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-mode-toggle-container" },
3128
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", { className: `jp-agent-mode-toggle ${inputMode === 'agent' ? 'jp-agent-mode-toggle--active' : ''}`, onClick: toggleMode, title: `${inputMode === 'agent' ? 'Agent' : 'Chat'} 모드 (⇧Tab)` },
3129
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement("svg", { className: "jp-agent-mode-icon", viewBox: "0 0 16 16", fill: "currentColor", width: "14", height: "14" }, inputMode === 'agent' ? (
3449
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", { className: `jp-agent-mode-toggle ${inputMode !== 'chat' ? 'jp-agent-mode-toggle--active' : ''}`, onClick: toggleMode, title: `${inputMode === 'chat' ? 'Chat' : inputMode === 'agent' ? 'Agent' : 'Agent V2'} 모드 (⇧Tab)` },
3450
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("svg", { className: "jp-agent-mode-icon", viewBox: "0 0 16 16", fill: "currentColor", width: "14", height: "14" }, inputMode === 'chat' ? (
3451
+ // 채팅 아이콘 (Chat 모드)
3452
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("path", { d: "M8 1C3.58 1 0 4.13 0 8c0 1.5.5 2.88 1.34 4.04L.5 15l3.37-.92A8.56 8.56 0 008 15c4.42 0 8-3.13 8-7s-3.58-7-8-7zM4.5 9a1 1 0 110-2 1 1 0 010 2zm3.5 0a1 1 0 110-2 1 1 0 010 2zm3.5 0a1 1 0 110-2 1 1 0 010 2z" })) : inputMode === 'agent' ? (
3130
3453
  // 무한대 아이콘 (Agent 모드)
3131
3454
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("path", { d: "M4.5 8c0-1.38 1.12-2.5 2.5-2.5.9 0 1.68.48 2.12 1.2L8 8l1.12 1.3c-.44.72-1.22 1.2-2.12 1.2-1.38 0-2.5-1.12-2.5-2.5zm6.88 1.3c.44-.72 1.22-1.2 2.12-1.2 1.38 0 2.5 1.12 2.5 2.5s-1.12 2.5-2.5 2.5c-.9 0-1.68-.48-2.12-1.2L12.5 10.6c.3.24.68.4 1.1.4.83 0 1.5-.67 1.5-1.5S14.43 8 13.6 8c-.42 0-.8.16-1.1.4l-1.12 1.3zM7 9.5c-.42 0-.8-.16-1.1-.4L4.78 7.8c-.44.72-1.22 1.2-2.12 1.2C1.28 9 .17 7.88.17 6.5S1.29 4 2.67 4c.9 0 1.68.48 2.12 1.2L5.9 6.5c-.3-.24-.68-.4-1.1-.4C3.97 6.1 3.3 6.77 3.3 7.6s.67 1.5 1.5 1.5c.42 0 .8-.16 1.1-.4l1.12-1.3L8 8l-1 1.5z" })) : (
3132
- // 채팅 아이콘 (Chat 모드)
3133
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement("path", { d: "M8 1C3.58 1 0 4.13 0 8c0 1.5.5 2.88 1.34 4.04L.5 15l3.37-.92A8.56 8.56 0 008 15c4.42 0 8-3.13 8-7s-3.58-7-8-7zM4.5 9a1 1 0 110-2 1 1 0 010 2zm3.5 0a1 1 0 110-2 1 1 0 010 2zm3.5 0a1 1 0 110-2 1 1 0 010 2z" }))),
3134
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-mode-label" }, inputMode === 'agent' ? 'Agent' : 'Chat'),
3455
+ // 아이콘 (Agent V2 모드)
3456
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("path", { d: "M8 0a8 8 0 100 16A8 8 0 008 0zm.5 11.5h-1v-1h1v1zm1.7-4.3c-.4.5-.7.8-.7 1.3v.5h-1v-.5c0-.8.4-1.3.8-1.8.4-.4.7-.7.7-1.2 0-.6-.4-1-1-1-.5 0-.9.3-1 .8l-1-.2c.2-.9 1-1.6 2-1.6 1.1 0 2 .8 2 1.9 0 .8-.4 1.3-.8 1.8z" }))),
3457
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-mode-label" }, inputMode === 'chat' ? 'Chat' : inputMode === 'agent' ? 'Agent' : 'Agent V2'),
3135
3458
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("svg", { className: "jp-agent-mode-chevron", viewBox: "0 0 16 16", fill: "currentColor", width: "12", height: "12" },
3136
3459
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("path", { d: "M4.47 5.47a.75.75 0 011.06 0L8 7.94l2.47-2.47a.75.75 0 111.06 1.06l-3 3a.75.75 0 01-1.06 0l-3-3a.75.75 0 010-1.06z" }))),
3137
3460
  showModeDropdown && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-mode-dropdown" },
@@ -3144,7 +3467,12 @@ SyntaxError: '(' was never closed
3144
3467
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("svg", { viewBox: "0 0 16 16", fill: "currentColor", width: "14", height: "14" },
3145
3468
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("path", { d: "M4.5 8c0-1.38 1.12-2.5 2.5-2.5.9 0 1.68.48 2.12 1.2L8 8l1.12 1.3c-.44.72-1.22 1.2-2.12 1.2-1.38 0-2.5-1.12-2.5-2.5z" })),
3146
3469
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null, "Agent"),
3147
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-mode-shortcut" }, "\uB178\uD2B8\uBD81 \uC790\uB3D9 \uC2E4\uD589"))))),
3470
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-mode-shortcut" }, "\uB178\uD2B8\uBD81 \uC790\uB3D9 \uC2E4\uD589")),
3471
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", { className: `jp-agent-mode-option ${inputMode === 'agent_v2' ? 'jp-agent-mode-option--selected' : ''}`, onClick: () => { setInputMode('agent_v2'); setShowModeDropdown(false); } },
3472
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("svg", { viewBox: "0 0 16 16", fill: "currentColor", width: "14", height: "14" },
3473
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("path", { d: "M8 0a8 8 0 100 16A8 8 0 008 0zm.5 11.5h-1v-1h1v1zm1.7-4.3c-.4.5-.7.8-.7 1.3v.5h-1v-.5c0-.8.4-1.3.8-1.8.4-.4.7-.7.7-1.2 0-.6-.4-1-1-1-.5 0-.9.3-1 .8l-1-.2c.2-.9 1-1.6 2-1.6 1.1 0 2 .8 2 1.9 0 .8-.4 1.3-.8 1.8z" })),
3474
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null, "Agent V2"),
3475
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-mode-shortcut" }, "Deep Agent (HITL)"))))),
3148
3476
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-mode-hints" },
3149
3477
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-mode-hint" }, "\u21E7Tab \uBAA8\uB4DC \uC804\uD658")))),
3150
3478
  fileSelectionMetadata && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_FileSelectionDialog__WEBPACK_IMPORTED_MODULE_8__.FileSelectionDialog, { filename: fileSelectionMetadata.pattern, options: fileSelectionMetadata.options, message: fileSelectionMetadata.message, onSelect: handleFileSelect, onCancel: handleFileSelectCancel }))));
@@ -3641,6 +3969,7 @@ const SettingsPanel = ({ onClose, onSave, currentConfig }) => {
3641
3969
  const [systemPrompt, setSystemPrompt] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(initConfig.systemPrompt || '');
3642
3970
  const [workspaceRoot, setWorkspaceRoot] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(initConfig.workspaceRoot || '');
3643
3971
  const [autoApprove, setAutoApprove] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(Boolean(initConfig.autoApprove));
3972
+ const [idleTimeoutMinutes, setIdleTimeoutMinutes] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(initConfig.idleTimeoutMinutes ?? 60);
3644
3973
  // Update state when currentConfig changes
3645
3974
  (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
3646
3975
  if (currentConfig) {
@@ -3664,6 +3993,7 @@ const SettingsPanel = ({ onClose, onSave, currentConfig }) => {
3664
3993
  setSystemPrompt(currentConfig.systemPrompt || (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_1__.getDefaultLLMConfig)().systemPrompt || '');
3665
3994
  setWorkspaceRoot(currentConfig.workspaceRoot || '');
3666
3995
  setAutoApprove(Boolean(currentConfig.autoApprove));
3996
+ setIdleTimeoutMinutes(currentConfig.idleTimeoutMinutes ?? 60);
3667
3997
  }
3668
3998
  }, [currentConfig]);
3669
3999
  // Helper: Build LLM config from state
@@ -3685,7 +4015,8 @@ const SettingsPanel = ({ onClose, onSave, currentConfig }) => {
3685
4015
  },
3686
4016
  workspaceRoot: workspaceRoot.trim() ? workspaceRoot.trim() : undefined,
3687
4017
  systemPrompt: systemPrompt && systemPrompt.trim() ? systemPrompt : undefined,
3688
- autoApprove
4018
+ autoApprove,
4019
+ idleTimeoutMinutes
3689
4020
  });
3690
4021
  // Handlers for multiple API keys
3691
4022
  const handleAddKey = () => {
@@ -3853,6 +4184,11 @@ const SettingsPanel = ({ onClose, onSave, currentConfig }) => {
3853
4184
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", { className: "jp-agent-settings-checkbox" },
3854
4185
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("input", { type: "checkbox", checked: autoApprove, onChange: (e) => setAutoApprove(e.target.checked), "data-testid": "auto-approve-checkbox" }),
3855
4186
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null, "\uC2B9\uC778 \uC5C6\uC774 \uBC14\uB85C \uC2E4\uD589 (\uCF54\uB4DC/\uD30C\uC77C/\uC178 \uD3EC\uD568)"))),
4187
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-settings-group" },
4188
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", { className: "jp-agent-settings-label", htmlFor: "jp-agent-idle-timeout" },
4189
+ "Idle Timeout (\uBD84)",
4190
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("small", { style: { fontWeight: 'normal', marginLeft: '8px', color: '#666' } }, "\uBE44\uD65C\uB3D9 \uC2DC \uC790\uB3D9 \uC885\uB8CC (0 = \uBE44\uD65C\uC131\uD654)")),
4191
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("input", { id: "jp-agent-idle-timeout", type: "number", className: "jp-agent-settings-input", value: idleTimeoutMinutes, onChange: (e) => setIdleTimeoutMinutes(Math.max(0, parseInt(e.target.value) || 0)), min: 0, max: 1440, placeholder: "60", style: { width: '120px' }, "data-testid": "idle-timeout-input" })),
3856
4192
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-settings-group" },
3857
4193
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", { className: "jp-agent-settings-label" },
3858
4194
  "System Prompt (LangChain)",
@@ -4008,6 +4344,8 @@ __webpack_require__.r(__webpack_exports__);
4008
4344
  /* harmony import */ var _plugins_cell_buttons_plugin__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./plugins/cell-buttons-plugin */ "./lib/plugins/cell-buttons-plugin.js");
4009
4345
  /* harmony import */ var _plugins_prompt_generation_plugin__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./plugins/prompt-generation-plugin */ "./lib/plugins/prompt-generation-plugin.js");
4010
4346
  /* harmony import */ var _plugins_save_interceptor_plugin__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./plugins/save-interceptor-plugin */ "./lib/plugins/save-interceptor-plugin.js");
4347
+ /* harmony import */ var _plugins_idle_monitor_plugin__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./plugins/idle-monitor-plugin */ "./lib/plugins/idle-monitor-plugin.js");
4348
+ /* harmony import */ var _plugins_lsp_bridge_plugin__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./plugins/lsp-bridge-plugin */ "./lib/plugins/lsp-bridge-plugin.js");
4011
4349
  /**
4012
4350
  * Jupyter Agent Extension Entry Point
4013
4351
  */
@@ -4016,6 +4354,8 @@ __webpack_require__.r(__webpack_exports__);
4016
4354
 
4017
4355
 
4018
4356
 
4357
+
4358
+
4019
4359
  // Import styles
4020
4360
  // import '../style/index.css';
4021
4361
  /**
@@ -4027,7 +4367,9 @@ const plugins = [
4027
4367
  _plugins_sidebar_plugin__WEBPACK_IMPORTED_MODULE_0__.sidebarPlugin,
4028
4368
  _plugins_cell_buttons_plugin__WEBPACK_IMPORTED_MODULE_1__.cellButtonsPlugin,
4029
4369
  _plugins_prompt_generation_plugin__WEBPACK_IMPORTED_MODULE_2__.promptGenerationPlugin,
4030
- _plugins_save_interceptor_plugin__WEBPACK_IMPORTED_MODULE_3__.saveInterceptorPlugin
4370
+ _plugins_save_interceptor_plugin__WEBPACK_IMPORTED_MODULE_3__.saveInterceptorPlugin,
4371
+ _plugins_idle_monitor_plugin__WEBPACK_IMPORTED_MODULE_4__.idleMonitorPlugin,
4372
+ _plugins_lsp_bridge_plugin__WEBPACK_IMPORTED_MODULE_5__.lspBridgePlugin
4031
4373
  ];
4032
4374
  /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (plugins);
4033
4375
 
@@ -4837,50 +5179,1010 @@ function showCustomPromptDialog(cell) {
4837
5179
  if (cellOutput) {
4838
5180
  llmPrompt += `\n\n실행 결과:\n\`\`\`\n${cellOutput}\n\`\`\``;
4839
5181
  }
4840
- const agentPanel = window._hdspAgentPanel;
4841
- if (agentPanel) {
4842
- // Activate the sidebar panel
4843
- const app = window.jupyterapp;
4844
- if (app) {
4845
- app.shell.activateById(agentPanel.id);
5182
+ const agentPanel = window._hdspAgentPanel;
5183
+ if (agentPanel) {
5184
+ // Activate the sidebar panel
5185
+ const app = window.jupyterapp;
5186
+ if (app) {
5187
+ app.shell.activateById(agentPanel.id);
5188
+ }
5189
+ // Send both prompts with cell ID and cell index
5190
+ if (agentPanel.addCellActionMessage) {
5191
+ // cellIndex is 1-based (for display), convert to 0-based for array access
5192
+ const cellIndexZeroBased = cellIndex - 1;
5193
+ agentPanel.addCellActionMessage(_types__WEBPACK_IMPORTED_MODULE_1__.CellAction.CUSTOM_PROMPT, cellContent, displayPrompt, llmPrompt, cellId, cellIndexZeroBased);
5194
+ }
5195
+ }
5196
+ };
5197
+ submitBtn.addEventListener('click', handleSubmit);
5198
+ submitBtn.addEventListener('mouseenter', () => {
5199
+ submitBtn.style.background = 'rgba(25, 118, 210, 0.1)';
5200
+ });
5201
+ submitBtn.addEventListener('mouseleave', () => {
5202
+ submitBtn.style.background = 'transparent';
5203
+ });
5204
+ // Enter 키로 제출 (Shift+Enter는 줄바꿈)
5205
+ inputField?.addEventListener('keydown', (e) => {
5206
+ if (e.key === 'Enter' && !e.shiftKey) {
5207
+ e.preventDefault();
5208
+ handleSubmit();
5209
+ }
5210
+ });
5211
+ // 오버레이 클릭 시 다이얼로그 닫기
5212
+ dialogOverlay.addEventListener('click', (e) => {
5213
+ if (e.target === dialogOverlay) {
5214
+ dialogOverlay.remove();
5215
+ }
5216
+ });
5217
+ // ESC 키로 다이얼로그 닫기
5218
+ const handleEscapeKey = (e) => {
5219
+ if (e.key === 'Escape') {
5220
+ dialogOverlay.remove();
5221
+ document.removeEventListener('keydown', handleEscapeKey);
5222
+ }
5223
+ };
5224
+ document.addEventListener('keydown', handleEscapeKey);
5225
+ }
5226
+
5227
+
5228
+ /***/ },
5229
+
5230
+ /***/ "./lib/plugins/idle-monitor-plugin.js"
5231
+ /*!********************************************!*\
5232
+ !*** ./lib/plugins/idle-monitor-plugin.js ***!
5233
+ \********************************************/
5234
+ (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
5235
+
5236
+ __webpack_require__.r(__webpack_exports__);
5237
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
5238
+ /* harmony export */ getIdleMonitor: () => (/* binding */ getIdleMonitor),
5239
+ /* harmony export */ idleMonitorPlugin: () => (/* binding */ idleMonitorPlugin)
5240
+ /* harmony export */ });
5241
+ /* harmony import */ var _jupyterlab_notebook__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @jupyterlab/notebook */ "webpack/sharing/consume/default/@jupyterlab/notebook");
5242
+ /* harmony import */ var _jupyterlab_notebook__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_jupyterlab_notebook__WEBPACK_IMPORTED_MODULE_0__);
5243
+ /* harmony import */ var _jupyterlab_terminal__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @jupyterlab/terminal */ "webpack/sharing/consume/default/@jupyterlab/terminal");
5244
+ /* harmony import */ var _jupyterlab_terminal__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_jupyterlab_terminal__WEBPACK_IMPORTED_MODULE_1__);
5245
+ /* harmony import */ var _jupyterlab_coreutils__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @jupyterlab/coreutils */ "webpack/sharing/consume/default/@jupyterlab/coreutils");
5246
+ /* harmony import */ var _jupyterlab_coreutils__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_jupyterlab_coreutils__WEBPACK_IMPORTED_MODULE_2__);
5247
+ /**
5248
+ * Idle Monitor Plugin
5249
+ *
5250
+ * Monitors user activity and triggers shutdown after idle timeout.
5251
+ * Activity is tracked by:
5252
+ * - Keyboard events
5253
+ * - Mouse events
5254
+ * - Notebook cell execution (kernel busy status)
5255
+ * - Terminal output
5256
+ */
5257
+
5258
+
5259
+
5260
+ /**
5261
+ * Plugin namespace
5262
+ */
5263
+ const PLUGIN_ID = '@hdsp-agent/idle-monitor';
5264
+ const CONFIG_STORAGE_KEY = 'hdsp-agent-llm-config';
5265
+ /**
5266
+ * Default idle timeout (in minutes)
5267
+ */
5268
+ const DEFAULT_IDLE_TIMEOUT_MINUTES = 60;
5269
+ // Check intervals - must be shorter than warning period to detect it
5270
+ const CHECK_INTERVAL_NORMAL_MS = 10 * 1000; // Check every 10 seconds (normal mode)
5271
+ const CHECK_INTERVAL_WARNING_MS = 1000; // Check every second (warning mode for countdown)
5272
+ // Warning period: show countdown before timeout (minimum 30 seconds, or 25% of timeout)
5273
+ const MIN_WARNING_PERIOD_MS = 30 * 1000; // Minimum 30 seconds warning
5274
+ /**
5275
+ * Idle Monitor Service
5276
+ */
5277
+ class IdleMonitorService {
5278
+ constructor() {
5279
+ this.checkIntervalId = null;
5280
+ this.countdownElement = null;
5281
+ this.isShuttingDown = false;
5282
+ this.notebookTracker = null;
5283
+ this.terminalTracker = null;
5284
+ this.isInWarningMode = false;
5285
+ this.isDisabled = false;
5286
+ /**
5287
+ * Throttle mousemove events to reduce performance impact
5288
+ */
5289
+ this.mouseMoveThrottleTime = 0;
5290
+ this.lastActivityTime = Date.now();
5291
+ // Initialize timeout from localStorage config
5292
+ this.idleTimeoutMinutes = this.loadTimeoutFromConfig();
5293
+ this.isDisabled = this.idleTimeoutMinutes === 0;
5294
+ this.idleTimeoutMs = this.idleTimeoutMinutes * 60 * 1000;
5295
+ // Warning period: 25% of timeout or minimum 30 seconds
5296
+ const warningPeriod = Math.max(MIN_WARNING_PERIOD_MS, this.idleTimeoutMs * 0.25);
5297
+ this.warningStartMs = this.idleTimeoutMs - warningPeriod;
5298
+ this.setupActivityListeners();
5299
+ this.setupStorageListener();
5300
+ this.createCountdownElement();
5301
+ if (!this.isDisabled) {
5302
+ this.startIdleCheck();
5303
+ }
5304
+ console.log('[IdleMonitor] Service initialized. Idle timeout:', this.idleTimeoutMinutes, 'minutes, warning at:', Math.round(this.warningStartMs / 1000), 'sec', this.isDisabled ? '(DISABLED)' : '');
5305
+ }
5306
+ /**
5307
+ * Load timeout configuration from localStorage
5308
+ */
5309
+ loadTimeoutFromConfig() {
5310
+ try {
5311
+ const stored = localStorage.getItem(CONFIG_STORAGE_KEY);
5312
+ if (stored) {
5313
+ const config = JSON.parse(stored);
5314
+ if (typeof config.idleTimeoutMinutes === 'number') {
5315
+ return config.idleTimeoutMinutes;
5316
+ }
5317
+ }
5318
+ }
5319
+ catch (e) {
5320
+ console.warn('[IdleMonitor] Failed to load config from localStorage:', e);
5321
+ }
5322
+ return DEFAULT_IDLE_TIMEOUT_MINUTES;
5323
+ }
5324
+ /**
5325
+ * Setup listener for localStorage changes (config updates from settings panel)
5326
+ */
5327
+ setupStorageListener() {
5328
+ window.addEventListener('storage', (e) => {
5329
+ if (e.key === CONFIG_STORAGE_KEY) {
5330
+ const newTimeout = this.loadTimeoutFromConfig();
5331
+ if (newTimeout !== this.idleTimeoutMinutes) {
5332
+ this.updateTimeout(newTimeout);
5333
+ }
5334
+ }
5335
+ });
5336
+ // Also listen for custom event (same-tab config changes)
5337
+ window.addEventListener('hdsp-config-updated', () => {
5338
+ const newTimeout = this.loadTimeoutFromConfig();
5339
+ if (newTimeout !== this.idleTimeoutMinutes) {
5340
+ this.updateTimeout(newTimeout);
5341
+ }
5342
+ });
5343
+ }
5344
+ /**
5345
+ * Update timeout dynamically
5346
+ */
5347
+ updateTimeout(minutes) {
5348
+ const wasDisabled = this.isDisabled;
5349
+ this.idleTimeoutMinutes = minutes;
5350
+ this.isDisabled = minutes === 0;
5351
+ this.idleTimeoutMs = minutes * 60 * 1000;
5352
+ // Warning period: 25% of timeout or minimum 30 seconds
5353
+ const warningPeriod = Math.max(MIN_WARNING_PERIOD_MS, this.idleTimeoutMs * 0.25);
5354
+ this.warningStartMs = this.idleTimeoutMs - warningPeriod;
5355
+ console.log('[IdleMonitor] Timeout updated to:', minutes, 'minutes', this.isDisabled ? '(DISABLED)' : '');
5356
+ // Stop idle check if disabled
5357
+ if (this.isDisabled) {
5358
+ this.stopIdleCheck();
5359
+ this.hideCountdown();
5360
+ }
5361
+ else if (wasDisabled) {
5362
+ // Re-enable if was disabled
5363
+ this.resetIdleTimer();
5364
+ this.startIdleCheck();
5365
+ }
5366
+ }
5367
+ /**
5368
+ * Stop idle check interval
5369
+ */
5370
+ stopIdleCheck() {
5371
+ if (this.checkIntervalId !== null) {
5372
+ clearInterval(this.checkIntervalId);
5373
+ this.checkIntervalId = null;
5374
+ }
5375
+ this.isInWarningMode = false;
5376
+ console.log('[IdleMonitor] Idle check stopped');
5377
+ }
5378
+ /**
5379
+ * Set notebook tracker for monitoring cell execution
5380
+ */
5381
+ setNotebookTracker(tracker) {
5382
+ this.notebookTracker = tracker;
5383
+ // Monitor cell execution changes
5384
+ tracker.currentChanged.connect(() => {
5385
+ this.resetIdleTimer();
5386
+ });
5387
+ // Monitor active cell changes
5388
+ tracker.activeCellChanged.connect(() => {
5389
+ this.resetIdleTimer();
5390
+ });
5391
+ console.log('[IdleMonitor] Notebook tracker connected');
5392
+ }
5393
+ /**
5394
+ * Set terminal tracker for monitoring terminal activity
5395
+ */
5396
+ setTerminalTracker(tracker) {
5397
+ this.terminalTracker = tracker;
5398
+ // Monitor terminal changes
5399
+ tracker.currentChanged.connect(() => {
5400
+ this.resetIdleTimer();
5401
+ });
5402
+ // Setup monitoring for existing terminals
5403
+ tracker.forEach(terminal => {
5404
+ this.setupTerminalMonitoring(terminal);
5405
+ });
5406
+ // Monitor new terminals
5407
+ tracker.widgetAdded.connect((_, terminal) => {
5408
+ this.setupTerminalMonitoring(terminal);
5409
+ });
5410
+ console.log('[IdleMonitor] Terminal tracker connected');
5411
+ }
5412
+ /**
5413
+ * Setup monitoring for a single terminal
5414
+ * Simply reset idle timer on any terminal output
5415
+ */
5416
+ setupTerminalMonitoring(terminal) {
5417
+ const terminalId = terminal.id || terminal.session?.name || 'unknown';
5418
+ const session = terminal.session;
5419
+ if (!session)
5420
+ return;
5421
+ // Listen for terminal output - any output resets idle timer
5422
+ session.messageReceived.connect(() => {
5423
+ this.resetIdleTimer();
5424
+ });
5425
+ console.log('[IdleMonitor] Terminal monitoring setup:', terminalId);
5426
+ }
5427
+ /**
5428
+ * Setup global activity listeners
5429
+ */
5430
+ setupActivityListeners() {
5431
+ // Keyboard events
5432
+ document.addEventListener('keydown', this.handleActivity.bind(this), true);
5433
+ document.addEventListener('keyup', this.handleActivity.bind(this), true);
5434
+ document.addEventListener('keypress', this.handleActivity.bind(this), true);
5435
+ // Mouse events
5436
+ document.addEventListener('mousedown', this.handleActivity.bind(this), true);
5437
+ document.addEventListener('mouseup', this.handleActivity.bind(this), true);
5438
+ document.addEventListener('mousemove', this.throttledMouseMove.bind(this), true);
5439
+ document.addEventListener('wheel', this.handleActivity.bind(this), true);
5440
+ document.addEventListener('click', this.handleActivity.bind(this), true);
5441
+ // Touch events (for tablet support)
5442
+ document.addEventListener('touchstart', this.handleActivity.bind(this), true);
5443
+ document.addEventListener('touchend', this.handleActivity.bind(this), true);
5444
+ console.log('[IdleMonitor] Activity listeners attached');
5445
+ }
5446
+ throttledMouseMove() {
5447
+ const now = Date.now();
5448
+ if (now - this.mouseMoveThrottleTime > 5000) { // Only update every 5 seconds for mouse move
5449
+ this.mouseMoveThrottleTime = now;
5450
+ this.handleActivity();
5451
+ }
5452
+ }
5453
+ /**
5454
+ * Handle any user activity
5455
+ */
5456
+ handleActivity() {
5457
+ this.resetIdleTimer();
5458
+ }
5459
+ /**
5460
+ * Reset the idle timer
5461
+ */
5462
+ resetIdleTimer() {
5463
+ this.lastActivityTime = Date.now();
5464
+ this.isShuttingDown = false;
5465
+ this.hideCountdown();
5466
+ // Switch back to normal mode if in warning mode
5467
+ this.switchToNormalMode();
5468
+ }
5469
+ /**
5470
+ * Create countdown display element (fixed position, top-right)
5471
+ */
5472
+ createCountdownElement() {
5473
+ // Create countdown container with fixed positioning
5474
+ this.countdownElement = document.createElement('div');
5475
+ this.countdownElement.id = 'hdsp-idle-countdown';
5476
+ this.countdownElement.className = 'hdsp-idle-countdown';
5477
+ this.countdownElement.style.cssText = `
5478
+ display: none;
5479
+ position: fixed;
5480
+ right: 20px;
5481
+ top: 10px;
5482
+ background: linear-gradient(135deg, #ff6b6b 0%, #ee5a5a 100%);
5483
+ color: white;
5484
+ padding: 8px 16px;
5485
+ border-radius: 20px;
5486
+ font-size: 14px;
5487
+ font-weight: 600;
5488
+ box-shadow: 0 4px 12px rgba(238, 90, 90, 0.5);
5489
+ z-index: 99999;
5490
+ white-space: nowrap;
5491
+ cursor: pointer;
5492
+ user-select: none;
5493
+ `;
5494
+ // Click to dismiss and reset timer
5495
+ this.countdownElement.addEventListener('click', () => {
5496
+ this.resetIdleTimer();
5497
+ console.log('[IdleMonitor] User clicked countdown - timer reset');
5498
+ });
5499
+ // Add pulse animation
5500
+ const style = document.createElement('style');
5501
+ style.id = 'hdsp-idle-countdown-style';
5502
+ style.textContent = `
5503
+ @keyframes hdsp-pulse {
5504
+ 0% { transform: scale(1); }
5505
+ 50% { transform: scale(1.03); }
5506
+ 100% { transform: scale(1); }
5507
+ }
5508
+
5509
+ .hdsp-idle-countdown {
5510
+ animation: hdsp-pulse 2s infinite;
5511
+ }
5512
+
5513
+ .hdsp-idle-countdown.warning {
5514
+ background: linear-gradient(135deg, #ffa726 0%, #fb8c00 100%) !important;
5515
+ box-shadow: 0 4px 12px rgba(251, 140, 0, 0.5) !important;
5516
+ }
5517
+
5518
+ .hdsp-idle-countdown.critical {
5519
+ background: linear-gradient(135deg, #ef5350 0%, #d32f2f 100%) !important;
5520
+ box-shadow: 0 4px 12px rgba(211, 47, 47, 0.6) !important;
5521
+ animation: hdsp-pulse-critical 0.5s infinite !important;
5522
+ }
5523
+
5524
+ @keyframes hdsp-pulse-critical {
5525
+ 0% { transform: scale(1); opacity: 1; }
5526
+ 50% { transform: scale(1.05); opacity: 0.9; }
5527
+ 100% { transform: scale(1); opacity: 1; }
5528
+ }
5529
+ `;
5530
+ // Remove existing style if any
5531
+ const existingStyle = document.getElementById('hdsp-idle-countdown-style');
5532
+ if (existingStyle) {
5533
+ existingStyle.remove();
5534
+ }
5535
+ document.head.appendChild(style);
5536
+ // Append to body for fixed positioning
5537
+ document.body.appendChild(this.countdownElement);
5538
+ console.log('[IdleMonitor] Countdown element created (fixed position)');
5539
+ }
5540
+ /**
5541
+ * Show countdown in toolbar
5542
+ */
5543
+ showCountdown(secondsRemaining) {
5544
+ console.log(`[IdleMonitor] showCountdown called: ${secondsRemaining}s remaining`);
5545
+ if (!this.countdownElement) {
5546
+ this.createCountdownElement();
5547
+ return;
5548
+ }
5549
+ this.countdownElement.style.display = 'block';
5550
+ this.countdownElement.textContent = `Idle Shutdown ${secondsRemaining}초 전`;
5551
+ // Update styling based on urgency
5552
+ this.countdownElement.classList.remove('warning', 'critical');
5553
+ if (secondsRemaining <= 10) {
5554
+ this.countdownElement.classList.add('critical');
5555
+ }
5556
+ else if (secondsRemaining <= 30) {
5557
+ this.countdownElement.classList.add('warning');
5558
+ }
5559
+ }
5560
+ /**
5561
+ * Hide countdown display
5562
+ */
5563
+ hideCountdown() {
5564
+ if (this.countdownElement) {
5565
+ this.countdownElement.style.display = 'none';
5566
+ }
5567
+ }
5568
+ /**
5569
+ * Check if any notebook cells are currently executing
5570
+ * Uses kernel status for reliability
5571
+ */
5572
+ hasRunningCells() {
5573
+ if (!this.notebookTracker)
5574
+ return false;
5575
+ // Check all open notebooks via kernel status
5576
+ const notebooks = this.notebookTracker.filter(() => true);
5577
+ for (const notebook of notebooks) {
5578
+ const session = notebook.sessionContext?.session;
5579
+ if (session) {
5580
+ const kernelStatus = session.kernel?.status;
5581
+ // Kernel is busy = cells are executing
5582
+ if (kernelStatus === 'busy') {
5583
+ return true;
5584
+ }
5585
+ }
5586
+ // Fallback: also check DOM for [*] indicator (for edge cases)
5587
+ if (notebook.content) {
5588
+ const cells = notebook.content.widgets;
5589
+ for (const cell of cells) {
5590
+ const model = cell.model;
5591
+ if (model && model.type === 'code') {
5592
+ const promptNode = cell.node.querySelector('.jp-InputPrompt');
5593
+ if (promptNode && promptNode.textContent?.includes('*')) {
5594
+ return true;
5595
+ }
5596
+ }
5597
+ }
5598
+ }
5599
+ }
5600
+ return false;
5601
+ }
5602
+ /**
5603
+ * Start idle checking interval
5604
+ * Starts in normal mode (10 min), switches to warning mode (1 sec) when needed
5605
+ */
5606
+ startIdleCheck() {
5607
+ this.checkIntervalId = window.setInterval(() => {
5608
+ this.checkIdleStatus();
5609
+ }, CHECK_INTERVAL_NORMAL_MS);
5610
+ console.log('[IdleMonitor] Started in normal mode: check every 10 seconds');
5611
+ }
5612
+ /**
5613
+ * Switch to warning mode (faster checks for countdown)
5614
+ */
5615
+ switchToWarningMode() {
5616
+ if (this.isInWarningMode)
5617
+ return;
5618
+ this.isInWarningMode = true;
5619
+ // Clear normal interval
5620
+ if (this.checkIntervalId !== null) {
5621
+ clearInterval(this.checkIntervalId);
5622
+ }
5623
+ // Start warning interval (1 second)
5624
+ this.checkIntervalId = window.setInterval(() => {
5625
+ this.checkIdleStatus();
5626
+ }, CHECK_INTERVAL_WARNING_MS);
5627
+ console.log('[IdleMonitor] Switched to warning mode: check every', CHECK_INTERVAL_WARNING_MS / 1000, 'seconds');
5628
+ }
5629
+ /**
5630
+ * Switch back to normal mode
5631
+ */
5632
+ switchToNormalMode() {
5633
+ if (!this.isInWarningMode)
5634
+ return;
5635
+ this.isInWarningMode = false;
5636
+ // Clear warning interval
5637
+ if (this.checkIntervalId !== null) {
5638
+ clearInterval(this.checkIntervalId);
5639
+ }
5640
+ // Start normal interval (10 minutes)
5641
+ this.checkIntervalId = window.setInterval(() => {
5642
+ this.checkIdleStatus();
5643
+ }, CHECK_INTERVAL_NORMAL_MS);
5644
+ console.log('[IdleMonitor] Switched to normal mode: check every 10 seconds');
5645
+ }
5646
+ /**
5647
+ * Check current idle status
5648
+ */
5649
+ checkIdleStatus() {
5650
+ // Skip if disabled
5651
+ if (this.isDisabled) {
5652
+ return;
5653
+ }
5654
+ // Skip if notebook cells are running (not idle)
5655
+ if (this.hasRunningCells()) {
5656
+ this.resetIdleTimer();
5657
+ return;
5658
+ }
5659
+ const now = Date.now();
5660
+ const idleTime = now - this.lastActivityTime;
5661
+ const remainingTime = this.idleTimeoutMs - idleTime;
5662
+ const secondsRemaining = Math.ceil(remainingTime / 1000);
5663
+ // Debug log every 10 seconds
5664
+ if (Math.floor(idleTime / 1000) % 10 === 0) {
5665
+ console.log(`[IdleMonitor] idle=${Math.round(idleTime / 1000)}s, warningAt=${Math.round(this.warningStartMs / 1000)}s, timeout=${Math.round(this.idleTimeoutMs / 1000)}s, remaining=${secondsRemaining}s`);
5666
+ }
5667
+ // Enter warning period - switch to fast checking for countdown
5668
+ if (idleTime >= this.warningStartMs && !this.isShuttingDown) {
5669
+ this.switchToWarningMode();
5670
+ this.showCountdown(secondsRemaining);
5671
+ }
5672
+ // Trigger shutdown if idle timeout reached
5673
+ if (idleTime >= this.idleTimeoutMs && !this.isShuttingDown) {
5674
+ this.isShuttingDown = true;
5675
+ this.triggerShutdown();
5676
+ }
5677
+ }
5678
+ /**
5679
+ * Trigger shutdown process
5680
+ */
5681
+ async triggerShutdown() {
5682
+ console.log('[IdleMonitor] Idle timeout reached. Triggering shutdown...');
5683
+ // Hide countdown
5684
+ this.hideCountdown();
5685
+ // Call shutdown API FIRST (non-blocking) - shutdown starts immediately
5686
+ this.callShutdownApi().catch(e => console.error('[IdleMonitor] Shutdown API error:', e));
5687
+ // Show notification popup (for user info only - shutdown already initiated)
5688
+ alert(`${this.idleTimeoutMinutes}분 동안 활동이 없어 세션이 종료됩니다.`);
5689
+ }
5690
+ /**
5691
+ * Call HDSP shutdown API via Jupyter server endpoint
5692
+ * Server-side call to avoid CORS issues
5693
+ */
5694
+ async callShutdownApi() {
5695
+ try {
5696
+ // Get base URL from PageConfig
5697
+ const baseUrl = _jupyterlab_coreutils__WEBPACK_IMPORTED_MODULE_2__.PageConfig.getBaseUrl();
5698
+ const apiUrl = `${baseUrl}hdsp-agent/idle-shutdown`;
5699
+ console.log('[IdleMonitor] Calling shutdown API via server:', apiUrl);
5700
+ // Call server endpoint (which will call HDSP API)
5701
+ const response = await fetch(apiUrl, {
5702
+ method: 'POST',
5703
+ headers: {
5704
+ 'Content-Type': 'application/json'
5705
+ }
5706
+ });
5707
+ const result = await response.json();
5708
+ if (response.ok && result.success) {
5709
+ console.log('[IdleMonitor] Shutdown API call successful:', result.message);
5710
+ }
5711
+ else {
5712
+ console.error('[IdleMonitor] Shutdown API call failed:', result.error || response.statusText);
5713
+ }
5714
+ }
5715
+ catch (error) {
5716
+ console.error('[IdleMonitor] Shutdown API call error:', error);
5717
+ }
5718
+ }
5719
+ /**
5720
+ * Get current idle time in milliseconds
5721
+ */
5722
+ getIdleTimeMs() {
5723
+ return Date.now() - this.lastActivityTime;
5724
+ }
5725
+ /**
5726
+ * Manually trigger activity (for external use)
5727
+ */
5728
+ triggerActivity() {
5729
+ this.resetIdleTimer();
5730
+ }
5731
+ /**
5732
+ * Cleanup service
5733
+ */
5734
+ dispose() {
5735
+ if (this.checkIntervalId !== null) {
5736
+ clearInterval(this.checkIntervalId);
5737
+ this.checkIntervalId = null;
5738
+ }
5739
+ if (this.countdownElement && this.countdownElement.parentNode) {
5740
+ this.countdownElement.parentNode.removeChild(this.countdownElement);
5741
+ }
5742
+ console.log('[IdleMonitor] Service disposed');
5743
+ }
5744
+ }
5745
+ /**
5746
+ * Global idle monitor instance
5747
+ */
5748
+ let idleMonitor = null;
5749
+ /**
5750
+ * Get idle monitor instance
5751
+ */
5752
+ function getIdleMonitor() {
5753
+ return idleMonitor;
5754
+ }
5755
+ /**
5756
+ * Idle Monitor Plugin
5757
+ */
5758
+ const idleMonitorPlugin = {
5759
+ id: PLUGIN_ID,
5760
+ autoStart: true,
5761
+ requires: [],
5762
+ optional: [_jupyterlab_notebook__WEBPACK_IMPORTED_MODULE_0__.INotebookTracker, _jupyterlab_terminal__WEBPACK_IMPORTED_MODULE_1__.ITerminalTracker],
5763
+ activate: (app, notebookTracker, terminalTracker) => {
5764
+ console.log('[IdleMonitorPlugin] Activating Idle Monitor');
5765
+ try {
5766
+ // Create idle monitor service
5767
+ idleMonitor = new IdleMonitorService();
5768
+ // Connect notebook tracker if available
5769
+ if (notebookTracker) {
5770
+ idleMonitor.setNotebookTracker(notebookTracker);
5771
+ }
5772
+ // Connect terminal tracker if available
5773
+ if (terminalTracker) {
5774
+ idleMonitor.setTerminalTracker(terminalTracker);
5775
+ }
5776
+ // Store reference globally for debugging
5777
+ window._hdspIdleMonitor = idleMonitor;
5778
+ console.log('[IdleMonitorPlugin] Idle Monitor activated successfully');
5779
+ }
5780
+ catch (error) {
5781
+ console.error('[IdleMonitorPlugin] Failed to activate:', error);
5782
+ }
5783
+ }
5784
+ };
5785
+
5786
+
5787
+ /***/ },
5788
+
5789
+ /***/ "./lib/plugins/lsp-bridge-plugin.js"
5790
+ /*!******************************************!*\
5791
+ !*** ./lib/plugins/lsp-bridge-plugin.js ***!
5792
+ \******************************************/
5793
+ (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
5794
+
5795
+ __webpack_require__.r(__webpack_exports__);
5796
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
5797
+ /* harmony export */ DiagnosticSeverity: () => (/* binding */ DiagnosticSeverity),
5798
+ /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__),
5799
+ /* harmony export */ getLSPBridge: () => (/* binding */ getLSPBridge),
5800
+ /* harmony export */ lspBridgePlugin: () => (/* binding */ lspBridgePlugin)
5801
+ /* harmony export */ });
5802
+ /* harmony import */ var _jupyterlab_notebook__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @jupyterlab/notebook */ "webpack/sharing/consume/default/@jupyterlab/notebook");
5803
+ /* harmony import */ var _jupyterlab_notebook__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_jupyterlab_notebook__WEBPACK_IMPORTED_MODULE_0__);
5804
+ /**
5805
+ * LSP Bridge Plugin
5806
+ *
5807
+ * jupyterlab-lsp와 HDSP Agent를 연결하는 브릿지
5808
+ * Crush 패턴 적용: Version-based 캐싱, 이벤트 기반 업데이트
5809
+ *
5810
+ * 주요 기능:
5811
+ * - LSP 진단 결과 캐싱 및 조회
5812
+ * - 심볼 참조 검색
5813
+ * - Agent 도구와 연동
5814
+ */
5815
+
5816
+ /**
5817
+ * Plugin namespace
5818
+ */
5819
+ const PLUGIN_ID = '@hdsp-agent/lsp-bridge';
5820
+ /**
5821
+ * LSP Diagnostic severity levels
5822
+ */
5823
+ var DiagnosticSeverity;
5824
+ (function (DiagnosticSeverity) {
5825
+ DiagnosticSeverity[DiagnosticSeverity["Error"] = 1] = "Error";
5826
+ DiagnosticSeverity[DiagnosticSeverity["Warning"] = 2] = "Warning";
5827
+ DiagnosticSeverity[DiagnosticSeverity["Information"] = 3] = "Information";
5828
+ DiagnosticSeverity[DiagnosticSeverity["Hint"] = 4] = "Hint";
5829
+ })(DiagnosticSeverity || (DiagnosticSeverity = {}));
5830
+ /**
5831
+ * LSP State Cache (Crush의 VersionedMap 패턴)
5832
+ * 버전 기반 캐시 무효화로 불필요한 재계산 방지
5833
+ */
5834
+ class LSPStateCache {
5835
+ constructor() {
5836
+ this.diagnosticsVersion = 0;
5837
+ this.diagnosticsCache = new Map();
5838
+ this.summaryCache = null;
5839
+ this.summaryVersion = -1;
5840
+ }
5841
+ /**
5842
+ * Update diagnostics for a file
5843
+ */
5844
+ updateDiagnostics(uri, diagnostics) {
5845
+ this.diagnosticsCache.set(uri, diagnostics);
5846
+ this.diagnosticsVersion++;
5847
+ }
5848
+ /**
5849
+ * Get diagnostics for a specific file or all files
5850
+ */
5851
+ getDiagnostics(uri) {
5852
+ if (uri) {
5853
+ return {
5854
+ version: this.diagnosticsVersion,
5855
+ diagnostics: this.diagnosticsCache.get(uri) || []
5856
+ };
5857
+ }
5858
+ return {
5859
+ version: this.diagnosticsVersion,
5860
+ diagnostics: this.diagnosticsCache
5861
+ };
5862
+ }
5863
+ /**
5864
+ * Get diagnostic counts with caching (Crush 패턴)
5865
+ */
5866
+ getDiagnosticSummary() {
5867
+ // Return cached if version matches
5868
+ if (this.summaryVersion === this.diagnosticsVersion && this.summaryCache) {
5869
+ return this.summaryCache;
5870
+ }
5871
+ // Recalculate
5872
+ let errors = 0;
5873
+ let warnings = 0;
5874
+ let hints = 0;
5875
+ for (const diags of this.diagnosticsCache.values()) {
5876
+ for (const d of diags) {
5877
+ switch (d.severity) {
5878
+ case DiagnosticSeverity.Error:
5879
+ errors++;
5880
+ break;
5881
+ case DiagnosticSeverity.Warning:
5882
+ warnings++;
5883
+ break;
5884
+ default:
5885
+ hints++;
5886
+ }
5887
+ }
5888
+ }
5889
+ this.summaryCache = {
5890
+ errors,
5891
+ warnings,
5892
+ hints,
5893
+ total: errors + warnings + hints,
5894
+ version: this.diagnosticsVersion
5895
+ };
5896
+ this.summaryVersion = this.diagnosticsVersion;
5897
+ return this.summaryCache;
5898
+ }
5899
+ /**
5900
+ * Clear diagnostics for a file
5901
+ */
5902
+ clearDiagnostics(uri) {
5903
+ if (this.diagnosticsCache.has(uri)) {
5904
+ this.diagnosticsCache.delete(uri);
5905
+ this.diagnosticsVersion++;
5906
+ }
5907
+ }
5908
+ /**
5909
+ * Clear all diagnostics
5910
+ */
5911
+ clearAll() {
5912
+ this.diagnosticsCache.clear();
5913
+ this.diagnosticsVersion++;
5914
+ }
5915
+ /**
5916
+ * Get current version
5917
+ */
5918
+ getVersion() {
5919
+ return this.diagnosticsVersion;
5920
+ }
5921
+ }
5922
+ /**
5923
+ * LSP Bridge Service
5924
+ * Agent 도구에서 LSP 기능을 사용할 수 있게 해주는 서비스
5925
+ */
5926
+ class LSPBridgeService {
5927
+ constructor() {
5928
+ this.cache = new LSPStateCache();
5929
+ this.lspAvailable = false;
5930
+ this.notebookTracker = null;
5931
+ this.checkLSPAvailability();
5932
+ }
5933
+ /**
5934
+ * Check if jupyterlab-lsp is available
5935
+ */
5936
+ async checkLSPAvailability() {
5937
+ try {
5938
+ // jupyterlab-lsp가 설치되어 있는지 확인
5939
+ // 실제로는 ILSPDocumentConnectionManager 토큰을 통해 확인
5940
+ this.lspAvailable = false; // 기본값, 실제 연결 시 true로 변경
5941
+ console.log('[LSPBridge] LSP availability check - will be updated when connection established');
5942
+ }
5943
+ catch (error) {
5944
+ console.warn('[LSPBridge] LSP not available:', error);
5945
+ this.lspAvailable = false;
5946
+ }
5947
+ }
5948
+ /**
5949
+ * Set notebook tracker for document access
5950
+ */
5951
+ setNotebookTracker(tracker) {
5952
+ this.notebookTracker = tracker;
5953
+ }
5954
+ /**
5955
+ * Mark LSP as available (called when connection is established)
5956
+ */
5957
+ setLSPAvailable(available) {
5958
+ this.lspAvailable = available;
5959
+ console.log('[LSPBridge] LSP available:', available);
5960
+ }
5961
+ /**
5962
+ * Check if LSP is available
5963
+ */
5964
+ isLSPAvailable() {
5965
+ return this.lspAvailable;
5966
+ }
5967
+ /**
5968
+ * Update diagnostics from external source (e.g., jupyterlab-lsp)
5969
+ */
5970
+ updateDiagnostics(uri, diagnostics) {
5971
+ this.cache.updateDiagnostics(uri, diagnostics);
5972
+ this.notifyDiagnosticsChanged(uri);
5973
+ }
5974
+ /**
5975
+ * Get diagnostics for Agent tool
5976
+ * Returns formatted diagnostics for a file or entire project
5977
+ */
5978
+ async getDiagnostics(filePath) {
5979
+ const result = this.cache.getDiagnostics(filePath ? this.pathToUri(filePath) : undefined);
5980
+ const summary = this.cache.getDiagnosticSummary();
5981
+ const formattedDiagnostics = [];
5982
+ if (filePath) {
5983
+ // Single file
5984
+ const diags = result.diagnostics;
5985
+ for (const d of diags) {
5986
+ formattedDiagnostics.push({
5987
+ severity: this.severityToString(d.severity),
5988
+ line: d.range.start.line + 1,
5989
+ character: d.range.start.character,
5990
+ message: d.message,
5991
+ source: d.source,
5992
+ code: d.code,
5993
+ file: filePath
5994
+ });
4846
5995
  }
4847
- // Send both prompts with cell ID and cell index
4848
- if (agentPanel.addCellActionMessage) {
4849
- // cellIndex is 1-based (for display), convert to 0-based for array access
4850
- const cellIndexZeroBased = cellIndex - 1;
4851
- agentPanel.addCellActionMessage(_types__WEBPACK_IMPORTED_MODULE_1__.CellAction.CUSTOM_PROMPT, cellContent, displayPrompt, llmPrompt, cellId, cellIndexZeroBased);
5996
+ }
5997
+ else {
5998
+ // All files
5999
+ const diagsMap = result.diagnostics;
6000
+ for (const [uri, diags] of diagsMap) {
6001
+ const file = this.uriToPath(uri);
6002
+ for (const d of diags) {
6003
+ formattedDiagnostics.push({
6004
+ severity: this.severityToString(d.severity),
6005
+ line: d.range.start.line + 1,
6006
+ character: d.range.start.character,
6007
+ message: d.message,
6008
+ source: d.source,
6009
+ code: d.code,
6010
+ file
6011
+ });
6012
+ }
4852
6013
  }
4853
6014
  }
4854
- };
4855
- submitBtn.addEventListener('click', handleSubmit);
4856
- submitBtn.addEventListener('mouseenter', () => {
4857
- submitBtn.style.background = 'rgba(25, 118, 210, 0.1)';
4858
- });
4859
- submitBtn.addEventListener('mouseleave', () => {
4860
- submitBtn.style.background = 'transparent';
4861
- });
4862
- // Enter 키로 제출 (Shift+Enter는 줄바꿈)
4863
- inputField?.addEventListener('keydown', (e) => {
4864
- if (e.key === 'Enter' && !e.shiftKey) {
4865
- e.preventDefault();
4866
- handleSubmit();
6015
+ // Sort: errors first, then by file and line (Crush 패턴)
6016
+ formattedDiagnostics.sort((a, b) => {
6017
+ const severityOrder = { error: 0, warning: 1, information: 2, hint: 3 };
6018
+ const severityDiff = (severityOrder[a.severity] || 3) - (severityOrder[b.severity] || 3);
6019
+ if (severityDiff !== 0)
6020
+ return severityDiff;
6021
+ const fileDiff = a.file.localeCompare(b.file);
6022
+ if (fileDiff !== 0)
6023
+ return fileDiff;
6024
+ return a.line - b.line;
6025
+ });
6026
+ return {
6027
+ success: true,
6028
+ diagnostics: formattedDiagnostics,
6029
+ summary
6030
+ };
6031
+ }
6032
+ /**
6033
+ * Find references for a symbol (Crush의 Grep-then-LSP 패턴)
6034
+ * 현재는 grep 기반으로 구현, LSP 연결 시 향상 가능
6035
+ */
6036
+ async getReferences(symbol, filePath, line, character) {
6037
+ // LSP가 없으면 grep 기반으로 검색하도록 안내
6038
+ if (!this.lspAvailable) {
6039
+ return {
6040
+ success: false,
6041
+ locations: []
6042
+ };
4867
6043
  }
4868
- });
4869
- // 오버레이 클릭 다이얼로그 닫기
4870
- dialogOverlay.addEventListener('click', (e) => {
4871
- if (e.target === dialogOverlay) {
4872
- dialogOverlay.remove();
6044
+ // TODO: jupyterlab-lsp 연결 시 구현
6045
+ // 현재는 결과 반환 (Agent가 execute_command_tool with grep 사용하도록)
6046
+ return {
6047
+ success: false,
6048
+ locations: []
6049
+ };
6050
+ }
6051
+ /**
6052
+ * Get diagnostic summary
6053
+ */
6054
+ getDiagnosticSummary() {
6055
+ return this.cache.getDiagnosticSummary();
6056
+ }
6057
+ /**
6058
+ * Notify diagnostics changed event
6059
+ */
6060
+ notifyDiagnosticsChanged(uri) {
6061
+ const summary = this.cache.getDiagnosticSummary();
6062
+ window.dispatchEvent(new CustomEvent('hdsp-lsp-diagnostics-changed', {
6063
+ detail: { uri, summary }
6064
+ }));
6065
+ }
6066
+ /**
6067
+ * Convert severity number to string
6068
+ */
6069
+ severityToString(severity) {
6070
+ switch (severity) {
6071
+ case DiagnosticSeverity.Error:
6072
+ return 'error';
6073
+ case DiagnosticSeverity.Warning:
6074
+ return 'warning';
6075
+ case DiagnosticSeverity.Information:
6076
+ return 'information';
6077
+ case DiagnosticSeverity.Hint:
6078
+ return 'hint';
6079
+ default:
6080
+ return 'hint';
4873
6081
  }
4874
- });
4875
- // ESC 키로 다이얼로그 닫기
4876
- const handleEscapeKey = (e) => {
4877
- if (e.key === 'Escape') {
4878
- dialogOverlay.remove();
4879
- document.removeEventListener('keydown', handleEscapeKey);
6082
+ }
6083
+ /**
6084
+ * Convert file path to URI
6085
+ */
6086
+ pathToUri(path) {
6087
+ if (path.startsWith('file://')) {
6088
+ return path;
4880
6089
  }
4881
- };
4882
- document.addEventListener('keydown', handleEscapeKey);
6090
+ return `file://${path.startsWith('/') ? '' : '/'}${path}`;
6091
+ }
6092
+ /**
6093
+ * Convert URI to file path
6094
+ */
6095
+ uriToPath(uri) {
6096
+ if (uri.startsWith('file://')) {
6097
+ return uri.replace('file://', '');
6098
+ }
6099
+ return uri;
6100
+ }
6101
+ /**
6102
+ * Dispose service
6103
+ */
6104
+ dispose() {
6105
+ this.cache.clearAll();
6106
+ console.log('[LSPBridge] Service disposed');
6107
+ }
6108
+ }
6109
+ /**
6110
+ * Global LSP Bridge instance
6111
+ */
6112
+ let lspBridge = null;
6113
+ /**
6114
+ * Get LSP Bridge instance
6115
+ */
6116
+ function getLSPBridge() {
6117
+ return lspBridge;
6118
+ }
6119
+ /**
6120
+ * LSP Bridge Plugin
6121
+ *
6122
+ * jupyterlab-lsp가 설치되어 있으면 연동하고,
6123
+ * 없으면 기본 기능만 제공
6124
+ */
6125
+ const lspBridgePlugin = {
6126
+ id: PLUGIN_ID,
6127
+ autoStart: true,
6128
+ requires: [],
6129
+ optional: [_jupyterlab_notebook__WEBPACK_IMPORTED_MODULE_0__.INotebookTracker],
6130
+ activate: (app, notebookTracker) => {
6131
+ console.log('[LSPBridgePlugin] Activating LSP Bridge...');
6132
+ // Create LSP Bridge service
6133
+ lspBridge = new LSPBridgeService();
6134
+ // Connect notebook tracker if available
6135
+ if (notebookTracker) {
6136
+ lspBridge.setNotebookTracker(notebookTracker);
6137
+ }
6138
+ // Store reference globally for debugging and Agent access
6139
+ window._hdspLSPBridge = lspBridge;
6140
+ // Try to connect to jupyterlab-lsp if available
6141
+ // This is done dynamically to avoid hard dependency
6142
+ tryConnectToLSP(app, lspBridge);
6143
+ console.log('[LSPBridgePlugin] LSP Bridge activated');
6144
+ }
6145
+ };
6146
+ /**
6147
+ * Try to connect to jupyterlab-lsp extension
6148
+ */
6149
+ async function tryConnectToLSP(app, bridge) {
6150
+ try {
6151
+ // Check if ILSPDocumentConnectionManager is available
6152
+ // This token is provided by jupyterlab-lsp
6153
+ const lspToken = '@jupyterlab/lsp:ILSPDocumentConnectionManager';
6154
+ // Try to get the LSP manager from the application
6155
+ // Note: This requires jupyterlab-lsp to be installed
6156
+ const hasLSP = app.commands.hasCommand('lsp:show-diagnostics-panel');
6157
+ if (hasLSP) {
6158
+ console.log('[LSPBridgePlugin] jupyterlab-lsp detected');
6159
+ bridge.setLSPAvailable(true);
6160
+ // Listen for LSP diagnostic events
6161
+ // jupyterlab-lsp publishes diagnostics through its own mechanism
6162
+ setupLSPEventListeners(bridge);
6163
+ }
6164
+ else {
6165
+ console.log('[LSPBridgePlugin] jupyterlab-lsp not detected, using fallback mode');
6166
+ bridge.setLSPAvailable(false);
6167
+ }
6168
+ }
6169
+ catch (error) {
6170
+ console.warn('[LSPBridgePlugin] Failed to connect to jupyterlab-lsp:', error);
6171
+ bridge.setLSPAvailable(false);
6172
+ }
4883
6173
  }
6174
+ /**
6175
+ * Setup event listeners for LSP events
6176
+ */
6177
+ function setupLSPEventListeners(bridge) {
6178
+ // jupyterlab-lsp uses its own event system
6179
+ // We'll listen for changes through polling or direct integration
6180
+ // This is a placeholder for future implementation
6181
+ console.log('[LSPBridgePlugin] LSP event listeners setup (placeholder)');
6182
+ // For now, we can use a simple polling mechanism or
6183
+ // hook into JupyterLab's document change events
6184
+ }
6185
+ /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (lspBridgePlugin);
4884
6186
 
4885
6187
 
4886
6188
  /***/ },
@@ -7735,23 +9037,23 @@ const STORAGE_KEY = 'hdsp-agent-llm-config';
7735
9037
  const DEFAULT_LANGCHAIN_SYSTEM_PROMPT = `You are an expert Python data scientist and Jupyter notebook assistant.
7736
9038
  Your role is to help users with data analysis, visualization, and Python coding tasks in Jupyter notebooks. You can use only Korean
7737
9039
 
7738
- ## ⚠️ CRITICAL RULE: NEVER produce an empty response
9040
+ # Core Behavior
9041
+ Be concise and direct. Answer in fewer than 4 lines unless the user asks for detail.
9042
+ After working on a file, just stop - don't explain what you did unless asked.
9043
+ Avoid unnecessary introductions or conclusions.
9044
+
9045
+ ## Task Management
9046
+ Use write_todos for complex multi-step tasks (3+ steps). Mark tasks in_progress before starting, completed immediately after finishing.
9047
+ For simple 1-2 step tasks, just do them directly without todos.
9048
+ When creating a todo list, ALWAYS include "작업 요약 및 다음단계 제시" as the LAST todo item.
7739
9049
 
7740
9050
  You MUST ALWAYS call a tool in every response. After any tool result, you MUST:
7741
9051
  1. Check your todo list - are there pending or in_progress items?
7742
9052
  2. If YES → call the next appropriate tool (jupyter_cell_tool, markdown_tool, etc.)
7743
- 3. When you suggest next steps for todo item '다음 단계 제시', you MUST create next steps in json format matching this schema:
7744
- {
7745
- "next_items": [
7746
- {
7747
- "subject": "<subject for next step>",
7748
- "description": "<detailed description for the next step>"
7749
- }, ...
7750
- ]
7751
- }
7752
- 4. If ALL todos are completed → call final_answer_tool with a summary
7753
-
7754
- NEVER end your turn without calling a tool. NEVER produce an empty response.
9053
+ 3. When executing "작업 요약 다음단계 제시" (final todo):
9054
+ - Output this JSON as text content: {"summary": "실행된 작업 요약", "next_items": [{"subject": "제목", "description": "설명"}]}
9055
+ - AND call write_todos to mark all as 'completed' IN THE SAME RESPONSE
9056
+ - Both content AND tool call must be in ONE response
7755
9057
 
7756
9058
  ## 🔴 MANDATORY: Resource Check Before Data Hanlding
7757
9059
  **ALWAYS call check_resource_tool FIRST** when the task involves:
@@ -7762,15 +9064,47 @@ NEVER end your turn without calling a tool. NEVER produce an empty response.
7762
9064
  ## Mandatory Workflow
7763
9065
  1. After EVERY tool result, immediately call the next tool
7764
9066
  2. Continue until ALL todos show status: "completed"
7765
- 3. ONLY THEN call final_answer_tool to summarize
9067
+ 3. When all todos are completed, the session ends automatically
7766
9068
  4. Only use jupyter_cell_tool for Python code or when the user explicitly asks to run in a notebook cell
7767
9069
  5. For plots and charts, use English text only.
7768
9070
 
7769
9071
  ## ❌ FORBIDDEN (will break the workflow)
7770
9072
  - Producing an empty response (no tool call, no content)
7771
9073
  - Stopping after any tool without calling the next tool
7772
- - Ending without calling final_answer_tool
7773
9074
  - Leaving todos in "in_progress" or "pending" state without continuing
9075
+
9076
+ ## 📂 File Search Best Practices
9077
+ **CRITICAL**: Use \`execute_command_tool\` with find/grep commands for file searching.
9078
+
9079
+ **To find files by NAME** (e.g., find titanic.csv):
9080
+ - \`execute_command_tool(command="find . -iname '*titanic*.csv' 2>/dev/null")\`
9081
+ - \`execute_command_tool(command="find . -name '*.csv' 2>/dev/null")\` - find all CSV files
9082
+
9083
+ **To search file CONTENTS** (e.g., find code containing "import pandas"):
9084
+ - \`execute_command_tool(command="grep -rn 'import pandas' --include='*.py' .")\`
9085
+ - \`execute_command_tool(command="grep -rn 'def train_model' --include='*.py' --include='*.ipynb' .")\`
9086
+
9087
+ ## 📖 File Reading Best Practices
9088
+ **CRITICAL**: When exploring codebases or reading files, use pagination to prevent context overflow.
9089
+
9090
+ **Pattern for codebase exploration:**
9091
+ 1. First scan: read_file_tool(path, limit=100) - See file structure and key sections
9092
+ 2. Targeted read: read_file_tool(path, offset=100, limit=200) - Read specific sections if needed
9093
+ 3. Full read: Only read without limit when necessary for immediate editing
9094
+
9095
+ **When to paginate (use offset/limit):**
9096
+ - Reading any file >500 lines
9097
+ - Exploring unfamiliar codebases (always start with limit=100)
9098
+ - Reading multiple files in sequence
9099
+ - Any research or investigation task
9100
+
9101
+ **When full read is OK:**
9102
+ - Small files (<500 lines)
9103
+ - Files you need to edit immediately after reading
9104
+ - After confirming file size with first scan
9105
+
9106
+ ## 🔧 Code Development
9107
+ For code generation/refactoring, use LSP tools (diagnostics_tool, references_tool) to check errors and find symbol usages. Use multiedit_file_tool for multiple changes in one file.
7774
9108
  `;
7775
9109
  // ═══════════════════════════════════════════════════════════════════════════
7776
9110
  // Key Rotation State (in-memory, not persisted)
@@ -7804,6 +9138,8 @@ function saveLLMConfig(config) {
7804
9138
  try {
7805
9139
  localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
7806
9140
  console.log('[ApiKeyManager] Config saved to localStorage');
9141
+ // Dispatch custom event for same-tab listeners (e.g., idle monitor)
9142
+ window.dispatchEvent(new CustomEvent('hdsp-config-updated', { detail: config }));
7807
9143
  }
7808
9144
  catch (e) {
7809
9145
  console.error('[ApiKeyManager] Failed to save config:', e);
@@ -8262,7 +9598,102 @@ class ApiService {
8262
9598
  * - Server receives ONLY ONE key per request
8263
9599
  * - On 429 rate limit, frontend rotates key and retries with next key
8264
9600
  */
8265
- async sendMessageStream(request, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall, onComplete, // Callback to capture thread_id for context persistence
9601
+ /**
9602
+ * 단순 Chat용 스트리밍 - /chat/stream 사용
9603
+ * 원래 main 브랜치의 구현 복원 - LangChain 에이전트 없이 단순 Q&A
9604
+ */
9605
+ async sendChatStream(request, onChunk, onMetadata) {
9606
+ const MAX_RETRIES = 10;
9607
+ let currentConfig = request.llmConfig;
9608
+ let lastError = null;
9609
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
9610
+ const requestToSend = currentConfig
9611
+ ? { ...request, llmConfig: (0,_ApiKeyManager__WEBPACK_IMPORTED_MODULE_0__.buildSingleKeyConfig)(currentConfig) }
9612
+ : request;
9613
+ try {
9614
+ await this.sendChatStreamInternal(requestToSend, onChunk, onMetadata);
9615
+ (0,_ApiKeyManager__WEBPACK_IMPORTED_MODULE_0__.resetKeyRotation)();
9616
+ return;
9617
+ }
9618
+ catch (error) {
9619
+ const errorMsg = error instanceof Error ? error.message : String(error);
9620
+ lastError = error instanceof Error ? error : new Error(errorMsg);
9621
+ if ((0,_ApiKeyManager__WEBPACK_IMPORTED_MODULE_0__.isRateLimitError)(errorMsg) && request.llmConfig) {
9622
+ console.log(`[ApiService] Chat rate limit on attempt ${attempt + 1}, trying next key...`);
9623
+ const rotatedConfig = (0,_ApiKeyManager__WEBPACK_IMPORTED_MODULE_0__.handleRateLimitError)(request.llmConfig);
9624
+ if (rotatedConfig) {
9625
+ currentConfig = request.llmConfig;
9626
+ continue;
9627
+ }
9628
+ else {
9629
+ throw new Error('모든 API 키가 Rate Limit 상태입니다. 잠시 후 다시 시도해주세요.');
9630
+ }
9631
+ }
9632
+ throw error;
9633
+ }
9634
+ }
9635
+ throw lastError || new Error('Maximum retry attempts exceeded');
9636
+ }
9637
+ /**
9638
+ * 단순 Chat 스트리밍 내부 구현 - /chat/stream 엔드포인트 사용
9639
+ */
9640
+ async sendChatStreamInternal(request, onChunk, onMetadata) {
9641
+ const response = await fetch(`${this.baseUrl}/chat/stream`, {
9642
+ method: 'POST',
9643
+ headers: this.getHeaders(),
9644
+ credentials: 'include',
9645
+ body: JSON.stringify(request)
9646
+ });
9647
+ if (!response.ok) {
9648
+ const error = await response.text();
9649
+ throw new Error(`Failed to send chat message: ${error}`);
9650
+ }
9651
+ const reader = response.body?.getReader();
9652
+ if (!reader) {
9653
+ throw new Error('Response body is not readable');
9654
+ }
9655
+ const decoder = new TextDecoder();
9656
+ let buffer = '';
9657
+ try {
9658
+ while (true) {
9659
+ const { done, value } = await reader.read();
9660
+ if (done)
9661
+ break;
9662
+ buffer += decoder.decode(value, { stream: true });
9663
+ const lines = buffer.split('\n');
9664
+ buffer = lines.pop() || '';
9665
+ for (const line of lines) {
9666
+ if (line.startsWith('data: ')) {
9667
+ try {
9668
+ const data = JSON.parse(line.slice(6));
9669
+ if (data.error) {
9670
+ throw new Error(data.error);
9671
+ }
9672
+ if (data.content) {
9673
+ onChunk(data.content);
9674
+ }
9675
+ if (data.done && data.conversationId && onMetadata) {
9676
+ onMetadata({ conversationId: data.conversationId });
9677
+ }
9678
+ }
9679
+ catch (e) {
9680
+ if (!(e instanceof SyntaxError)) {
9681
+ throw e;
9682
+ }
9683
+ }
9684
+ }
9685
+ }
9686
+ }
9687
+ }
9688
+ finally {
9689
+ reader.releaseLock();
9690
+ }
9691
+ }
9692
+ /**
9693
+ * Agent V2용 스트리밍 - /agent/langchain/stream 사용
9694
+ * LangChain Deep Agent (HITL, Todo, 도구 실행 등)
9695
+ */
9696
+ async sendAgentV2Stream(request, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall, onComplete, // Callback to capture thread_id for context persistence
8266
9697
  threadId // Optional thread_id to continue existing conversation
8267
9698
  ) {
8268
9699
  // Maximum retry attempts (should match number of keys)
@@ -8275,7 +9706,7 @@ class ApiService {
8275
9706
  ? { ...request, llmConfig: (0,_ApiKeyManager__WEBPACK_IMPORTED_MODULE_0__.buildSingleKeyConfig)(currentConfig) }
8276
9707
  : request;
8277
9708
  try {
8278
- await this.sendMessageStreamInternal(requestToSend, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall, onComplete, threadId);
9709
+ await this.sendAgentV2StreamInternal(requestToSend, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall, onComplete, threadId);
8279
9710
  // Success - reset key rotation state
8280
9711
  (0,_ApiKeyManager__WEBPACK_IMPORTED_MODULE_0__.resetKeyRotation)();
8281
9712
  return;
@@ -8322,10 +9753,18 @@ class ApiService {
8322
9753
  };
8323
9754
  }
8324
9755
  /**
8325
- * Internal streaming implementation (without retry logic)
8326
- * Uses LangChain agent endpoint for improved middleware support
9756
+ * 기존 sendMessageStream 유지 (하위 호환성)
9757
+ * 내부적으로 sendAgentV2Stream 호출
9758
+ * @deprecated sendChatStream 또는 sendAgentV2Stream을 직접 사용하세요
9759
+ */
9760
+ async sendMessageStream(request, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall, onComplete, threadId) {
9761
+ return this.sendAgentV2Stream(request, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall, onComplete, threadId);
9762
+ }
9763
+ /**
9764
+ * Agent V2 스트리밍 내부 구현 - /agent/langchain/stream 사용
9765
+ * (기존 sendMessageStreamInternal에서 이름 변경)
8327
9766
  */
8328
- async sendMessageStreamInternal(request, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall, onComplete, threadId) {
9767
+ async sendAgentV2StreamInternal(request, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall, onComplete, threadId) {
8329
9768
  // Convert IChatRequest to LangChain AgentRequest format
8330
9769
  // Frontend's context has limited fields, map what's available
8331
9770
  const resourceContext = await this.getResourceUsageSnapshot();
@@ -8713,6 +10152,24 @@ class ApiService {
8713
10152
  }
8714
10153
  return result;
8715
10154
  }
10155
+ async readFile(path, options) {
10156
+ const response = await fetch(`${this.baseUrl}/read-file`, {
10157
+ method: 'POST',
10158
+ headers: this.getHeaders(),
10159
+ credentials: 'include',
10160
+ body: JSON.stringify({
10161
+ path,
10162
+ encoding: options?.encoding || 'utf-8',
10163
+ cwd: options?.cwd
10164
+ })
10165
+ });
10166
+ const payload = await response.json().catch(() => ({}));
10167
+ if (!response.ok) {
10168
+ const errorMessage = payload.error || 'Failed to read file';
10169
+ return { success: false, error: errorMessage };
10170
+ }
10171
+ return payload;
10172
+ }
8716
10173
  async writeFile(path, content, options) {
8717
10174
  const response = await fetch(`${this.baseUrl}/write-file`, {
8718
10175
  method: 'POST',
@@ -8799,6 +10256,14 @@ class ApiService {
8799
10256
  });
8800
10257
  const payload = await response.json().catch(() => ({}));
8801
10258
  if (!response.ok) {
10259
+ if (response.status === 404) {
10260
+ return {
10261
+ success: false,
10262
+ files: [],
10263
+ dataframes: [],
10264
+ error: 'Resource check endpoint is not available on this Jupyter server.'
10265
+ };
10266
+ }
8802
10267
  const errorMessage = payload.error || 'Failed to check resources';
8803
10268
  throw new Error(errorMessage);
8804
10269
  }
@@ -10706,7 +12171,7 @@ class ToolExecutor {
10706
12171
  return { text, truncated: lines.length > maxLines };
10707
12172
  }
10708
12173
  /**
10709
- * read_file 도구: 파일 읽기
12174
+ * read_file 도구: 파일 읽기 (offset/limit 지원)
10710
12175
  */
10711
12176
  async executeReadFile(params) {
10712
12177
  console.log('[ToolExecutor] executeReadFile:', params);
@@ -10716,15 +12181,29 @@ class ToolExecutor {
10716
12181
  return { success: false, error: pathCheck.error };
10717
12182
  }
10718
12183
  const encoding = params.encoding || 'utf-8';
10719
- const maxLines = params.maxLines || 1000;
10720
- // Python 코드로 파일 읽기 (커널에서 실행)
12184
+ // Support both old (maxLines) and new (offset/limit) parameters
12185
+ const offset = typeof params.offset === 'number' ? Math.max(0, params.offset) : 0;
12186
+ const limit = typeof params.limit === 'number'
12187
+ ? params.limit
12188
+ : (typeof params.maxLines === 'number' ? params.maxLines : 500);
12189
+ // Python 코드로 파일 읽기 (커널에서 실행) - offset/limit 적용
10721
12190
  const pythonCode = `
10722
12191
  import json
10723
12192
  try:
10724
12193
  with open(${JSON.stringify(params.path)}, 'r', encoding=${JSON.stringify(encoding)}) as f:
10725
- lines = f.readlines()[:${maxLines}]
10726
- content = ''.join(lines)
10727
- result = {'success': True, 'content': content, 'lineCount': len(lines), 'truncated': len(lines) >= ${maxLines}}
12194
+ all_lines = f.readlines()
12195
+ total_lines = len(all_lines)
12196
+ sliced_lines = all_lines[${offset}:${offset + limit}]
12197
+ content = ''.join(sliced_lines)
12198
+ result = {
12199
+ 'success': True,
12200
+ 'content': content,
12201
+ 'lineCount': len(sliced_lines),
12202
+ 'totalLines': total_lines,
12203
+ 'offset': ${offset},
12204
+ 'limit': ${limit},
12205
+ 'truncated': total_lines > ${offset + limit}
12206
+ }
10728
12207
  except FileNotFoundError:
10729
12208
  result = {'success': False, 'error': f'File not found: ${params.path}'}
10730
12209
  except PermissionError:
@@ -13513,9 +14992,11 @@ class SafetyChecker {
13513
14992
 
13514
14993
  __webpack_require__.r(__webpack_exports__);
13515
14994
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
14995
+ /* harmony export */ countDiffLines: () => (/* binding */ countDiffLines),
13516
14996
  /* harmony export */ escapeHtml: () => (/* binding */ escapeHtml),
13517
14997
  /* harmony export */ formatInlineMarkdown: () => (/* binding */ formatInlineMarkdown),
13518
14998
  /* harmony export */ formatMarkdownToHtml: () => (/* binding */ formatMarkdownToHtml),
14999
+ /* harmony export */ highlightDiff: () => (/* binding */ highlightDiff),
13519
15000
  /* harmony export */ highlightJavaScript: () => (/* binding */ highlightJavaScript),
13520
15001
  /* harmony export */ highlightPython: () => (/* binding */ highlightPython),
13521
15002
  /* harmony export */ normalizeIndentation: () => (/* binding */ normalizeIndentation),
@@ -13589,19 +15070,19 @@ function extractNextItemsBlock(text) {
13589
15070
  if (lang && lang !== 'json' && !content.includes('"next_items"')) {
13590
15071
  continue;
13591
15072
  }
13592
- const items = parseNextItemsPayload(content);
13593
- if (items) {
15073
+ const parsed = parseNextItemsPayload(content);
15074
+ if (parsed) {
13594
15075
  const placeholder = `__NEXT_ITEMS_${Math.random().toString(36).slice(2, 11)}__`;
13595
15076
  const updated = text.slice(0, match.index) + placeholder + text.slice(match.index + match[0].length);
13596
- return { items, placeholder, text: updated };
15077
+ return { items: parsed.items, summary: parsed.summary, placeholder, text: updated };
13597
15078
  }
13598
15079
  }
13599
15080
  const range = findNextItemsJsonRange(text);
13600
15081
  if (!range)
13601
15082
  return null;
13602
15083
  const candidate = text.slice(range.start, range.end + 1);
13603
- const items = parseNextItemsPayload(candidate);
13604
- if (!items)
15084
+ const parsed = parseNextItemsPayload(candidate);
15085
+ if (!parsed)
13605
15086
  return null;
13606
15087
  let replacementStart = range.start;
13607
15088
  const lineStart = text.lastIndexOf('\n', range.start - 1) + 1;
@@ -13612,7 +15093,7 @@ function extractNextItemsBlock(text) {
13612
15093
  }
13613
15094
  const placeholder = `__NEXT_ITEMS_${Math.random().toString(36).slice(2, 11)}__`;
13614
15095
  const updated = text.slice(0, replacementStart) + placeholder + text.slice(range.end + 1);
13615
- return { items, placeholder, text: updated };
15096
+ return { items: parsed.items, summary: parsed.summary, placeholder, text: updated };
13616
15097
  }
13617
15098
  function findNextItemsJsonRange(text) {
13618
15099
  const key = '"next_items"';
@@ -13690,17 +15171,25 @@ function parseNextItemsPayload(payload) {
13690
15171
  return { subject, description };
13691
15172
  })
13692
15173
  .filter((item) => Boolean(item));
13693
- return items.length > 0 ? items : null;
15174
+ // Extract summary field if present
15175
+ const summaryRaw = parsed.summary;
15176
+ const summary = typeof summaryRaw === 'string' && summaryRaw.trim() ? summaryRaw.trim() : null;
15177
+ return items.length > 0 ? { items, summary } : null;
13694
15178
  }
13695
15179
  catch {
13696
15180
  return null;
13697
15181
  }
13698
15182
  }
13699
- function renderNextItemsList(items) {
13700
- // Simple arrow icon
15183
+ function renderNextItemsList(items, summary) {
15184
+ // Simple arrow icon for next items
13701
15185
  const arrowSvg = `
13702
15186
  <svg class="jp-next-items-icon" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
13703
15187
  <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"/>
15188
+ </svg>`;
15189
+ // Checkmark icon for summary
15190
+ const checkSvg = `
15191
+ <svg class="jp-summary-icon" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
15192
+ <path fill-rule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clip-rule="evenodd"/>
13704
15193
  </svg>`;
13705
15194
  const listItems = items.map((item) => {
13706
15195
  const subject = escapeHtml(item.subject);
@@ -13716,7 +15205,16 @@ function renderNextItemsList(items) {
13716
15205
  ${arrowSvg}
13717
15206
  </li>`;
13718
15207
  }).join('');
13719
- return `
15208
+ // Render summary as separate block above next items
15209
+ const summaryHtml = summary ? `
15210
+ <div class="jp-summary-block">
15211
+ <div class="jp-summary-header">작업 요약</div>
15212
+ <div class="jp-summary-body">
15213
+ ${checkSvg}
15214
+ <div class="jp-summary-content">${escapeHtml(summary)}</div>
15215
+ </div>
15216
+ </div>` : '';
15217
+ return `${summaryHtml}
13720
15218
  <div class="jp-next-items" data-next-items="true">
13721
15219
  <div class="jp-next-items-header">다음 단계 제안</div>
13722
15220
  <ul class="jp-next-items-list" role="list">
@@ -13849,6 +15347,52 @@ function highlightPython(code) {
13849
15347
  });
13850
15348
  return highlighted;
13851
15349
  }
15350
+ /**
15351
+ * Highlight diff output with colors for additions/deletions
15352
+ */
15353
+ function highlightDiff(code) {
15354
+ const lines = code.split('\n');
15355
+ const highlightedLines = lines.map(line => {
15356
+ const escapedLine = escapeHtml(line);
15357
+ // File headers (--- and +++)
15358
+ if (line.startsWith('---') || line.startsWith('+++')) {
15359
+ return `<span class="diff-header">${escapedLine}</span>`;
15360
+ }
15361
+ // Hunk headers (@@ ... @@)
15362
+ if (line.startsWith('@@')) {
15363
+ return `<span class="diff-hunk">${escapedLine}</span>`;
15364
+ }
15365
+ // Added lines
15366
+ if (line.startsWith('+')) {
15367
+ return `<span class="diff-add">${escapedLine}</span>`;
15368
+ }
15369
+ // Removed lines
15370
+ if (line.startsWith('-')) {
15371
+ return `<span class="diff-del">${escapedLine}</span>`;
15372
+ }
15373
+ // Context lines (unchanged)
15374
+ return `<span class="diff-context">${escapedLine}</span>`;
15375
+ });
15376
+ return highlightedLines.join('\n');
15377
+ }
15378
+ /**
15379
+ * Count additions and deletions in diff code
15380
+ */
15381
+ function countDiffLines(code) {
15382
+ const lines = code.split('\n');
15383
+ let additions = 0;
15384
+ let deletions = 0;
15385
+ for (const line of lines) {
15386
+ // Skip file headers
15387
+ if (line.startsWith('+++') || line.startsWith('---'))
15388
+ continue;
15389
+ if (line.startsWith('+'))
15390
+ additions++;
15391
+ else if (line.startsWith('-'))
15392
+ deletions++;
15393
+ }
15394
+ return { additions, deletions };
15395
+ }
13852
15396
  /**
13853
15397
  * Highlight JavaScript code
13854
15398
  */
@@ -14155,16 +15699,29 @@ function formatMarkdownToHtml(text) {
14155
15699
  language: lang
14156
15700
  });
14157
15701
  // Create HTML for code block
14158
- const highlightedCode = lang === 'python' || lang === 'py'
14159
- ? highlightPython(trimmedCode)
14160
- : lang === 'javascript' || lang === 'js'
14161
- ? highlightJavaScript(trimmedCode)
14162
- : escapeHtml(trimmedCode);
14163
- const htmlBlock = '<div class="code-block-container" data-block-id="' + blockId + '">' +
15702
+ const isDiff = lang === 'diff';
15703
+ const highlightedCode = isDiff
15704
+ ? highlightDiff(trimmedCode)
15705
+ : lang === 'python' || lang === 'py'
15706
+ ? highlightPython(trimmedCode)
15707
+ : lang === 'javascript' || lang === 'js'
15708
+ ? highlightJavaScript(trimmedCode)
15709
+ : escapeHtml(trimmedCode);
15710
+ // For diff blocks, calculate and show line counts
15711
+ let diffLineCountHtml = '';
15712
+ if (isDiff) {
15713
+ const { additions, deletions } = countDiffLines(trimmedCode);
15714
+ diffLineCountHtml = '<span class="diff-line-counts">' +
15715
+ '<span class="diff-additions">+' + additions + '</span>' +
15716
+ '<span class="diff-deletions">-' + deletions + '</span>' +
15717
+ '</span>';
15718
+ }
15719
+ const htmlBlock = '<div class="code-block-container' + (isDiff ? ' diff-block' : '') + '" data-block-id="' + blockId + '">' +
14164
15720
  '<div class="code-block-header">' +
14165
15721
  '<span class="code-block-language">' + escapeHtml(lang) + '</span>' +
15722
+ diffLineCountHtml +
14166
15723
  '<div class="code-block-actions">' +
14167
- '<button class="code-block-apply" data-block-id="' + blockId + '" title="셀에 적용">셀에 적용</button>' +
15724
+ (isDiff ? '' : '<button class="code-block-apply" data-block-id="' + blockId + '" title="셀에 적용">셀에 적용</button>') +
14168
15725
  '<button class="code-block-copy" data-block-id="' + blockId + '" title="복사">복사</button>' +
14169
15726
  '</div>' +
14170
15727
  '</div>' +
@@ -14283,7 +15840,7 @@ function formatMarkdownToHtml(text) {
14283
15840
  });
14284
15841
  // Step 8.5: Restore next items list placeholders
14285
15842
  if (nextItemsBlock) {
14286
- html = html.split(nextItemsBlock.placeholder).join(renderNextItemsList(nextItemsBlock.items));
15843
+ html = html.split(nextItemsBlock.placeholder).join(renderNextItemsList(nextItemsBlock.items, nextItemsBlock.summary));
14287
15844
  }
14288
15845
  return html;
14289
15846
  }
@@ -14446,4 +16003,4 @@ __webpack_require__.r(__webpack_exports__);
14446
16003
  /***/ }
14447
16004
 
14448
16005
  }]);
14449
- //# sourceMappingURL=lib_index_js.29cf4312af19e86f82af.js.map
16006
+ //# sourceMappingURL=lib_index_js.dc6434bee96ab03a0539.js.map