hdsp-jupyter-extension 2.0.6__py3-none-any.whl → 2.0.8__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 (88) hide show
  1. agent_server/core/embedding_service.py +67 -46
  2. agent_server/core/rag_manager.py +31 -17
  3. agent_server/core/reflection_engine.py +0 -1
  4. agent_server/core/retriever.py +13 -8
  5. agent_server/core/vllm_embedding_service.py +243 -0
  6. agent_server/knowledge/watchdog_service.py +1 -1
  7. agent_server/langchain/ARCHITECTURE.md +1193 -0
  8. agent_server/langchain/agent.py +82 -588
  9. agent_server/langchain/custom_middleware.py +663 -0
  10. agent_server/langchain/executors/__init__.py +2 -7
  11. agent_server/langchain/executors/notebook_searcher.py +46 -38
  12. agent_server/langchain/hitl_config.py +71 -0
  13. agent_server/langchain/llm_factory.py +166 -0
  14. agent_server/langchain/logging_utils.py +223 -0
  15. agent_server/langchain/prompts.py +150 -0
  16. agent_server/langchain/state.py +16 -6
  17. agent_server/langchain/tools/__init__.py +19 -0
  18. agent_server/langchain/tools/file_tools.py +354 -114
  19. agent_server/langchain/tools/file_utils.py +334 -0
  20. agent_server/langchain/tools/jupyter_tools.py +18 -18
  21. agent_server/langchain/tools/lsp_tools.py +264 -0
  22. agent_server/langchain/tools/resource_tools.py +161 -0
  23. agent_server/langchain/tools/search_tools.py +198 -216
  24. agent_server/langchain/tools/shell_tools.py +54 -0
  25. agent_server/main.py +11 -1
  26. agent_server/routers/health.py +1 -1
  27. agent_server/routers/langchain_agent.py +1040 -289
  28. agent_server/routers/rag.py +8 -3
  29. hdsp_agent_core/models/rag.py +15 -1
  30. hdsp_agent_core/prompts/auto_agent_prompts.py +3 -3
  31. hdsp_agent_core/services/rag_service.py +6 -1
  32. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/build_log.json +1 -1
  33. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/package.json +3 -2
  34. hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.02d346171474a0fb2dc1.js → hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.8740a527757068814573.js +470 -7
  35. hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.8740a527757068814573.js.map +1 -0
  36. hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.a223ea20056954479ae9.js → hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.e4ff4b5779b5e049f84c.js +3196 -441
  37. hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.e4ff4b5779b5e049f84c.js.map +1 -0
  38. hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.addf2fa038fa60304aa2.js → hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.020cdb0b864cfaa4e41e.js +9 -7
  39. hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.020cdb0b864cfaa4e41e.js.map +1 -0
  40. {hdsp_jupyter_extension-2.0.6.dist-info → hdsp_jupyter_extension-2.0.8.dist-info}/METADATA +2 -1
  41. {hdsp_jupyter_extension-2.0.6.dist-info → hdsp_jupyter_extension-2.0.8.dist-info}/RECORD +75 -69
  42. jupyter_ext/__init__.py +18 -0
  43. jupyter_ext/_version.py +1 -1
  44. jupyter_ext/handlers.py +1351 -58
  45. jupyter_ext/labextension/build_log.json +1 -1
  46. jupyter_ext/labextension/package.json +3 -2
  47. jupyter_ext/labextension/static/{frontend_styles_index_js.02d346171474a0fb2dc1.js → frontend_styles_index_js.8740a527757068814573.js} +470 -7
  48. jupyter_ext/labextension/static/frontend_styles_index_js.8740a527757068814573.js.map +1 -0
  49. jupyter_ext/labextension/static/{lib_index_js.a223ea20056954479ae9.js → lib_index_js.e4ff4b5779b5e049f84c.js} +3196 -441
  50. jupyter_ext/labextension/static/lib_index_js.e4ff4b5779b5e049f84c.js.map +1 -0
  51. jupyter_ext/labextension/static/{remoteEntry.addf2fa038fa60304aa2.js → remoteEntry.020cdb0b864cfaa4e41e.js} +9 -7
  52. jupyter_ext/labextension/static/remoteEntry.020cdb0b864cfaa4e41e.js.map +1 -0
  53. jupyter_ext/resource_usage.py +180 -0
  54. jupyter_ext/tests/test_handlers.py +58 -0
  55. agent_server/langchain/executors/jupyter_executor.py +0 -429
  56. agent_server/langchain/middleware/__init__.py +0 -36
  57. agent_server/langchain/middleware/code_search_middleware.py +0 -278
  58. agent_server/langchain/middleware/error_handling_middleware.py +0 -338
  59. agent_server/langchain/middleware/jupyter_execution_middleware.py +0 -301
  60. agent_server/langchain/middleware/rag_middleware.py +0 -227
  61. agent_server/langchain/middleware/validation_middleware.py +0 -240
  62. hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.02d346171474a0fb2dc1.js.map +0 -1
  63. hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.a223ea20056954479ae9.js.map +0 -1
  64. hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.addf2fa038fa60304aa2.js.map +0 -1
  65. jupyter_ext/labextension/static/frontend_styles_index_js.02d346171474a0fb2dc1.js.map +0 -1
  66. jupyter_ext/labextension/static/lib_index_js.a223ea20056954479ae9.js.map +0 -1
  67. jupyter_ext/labextension/static/remoteEntry.addf2fa038fa60304aa2.js.map +0 -1
  68. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/etc/jupyter/jupyter_server_config.d/hdsp_jupyter_extension.json +0 -0
  69. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/install.json +0 -0
  70. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.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
  71. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.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
  72. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.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
  73. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.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
  74. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/style.js +0 -0
  75. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.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
  76. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.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
  77. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js +0 -0
  78. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js.map +0 -0
  79. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js +0 -0
  80. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js.map +0 -0
  81. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_styled_dist_emotion-styled_browser_development_esm_js.661fb5836f4978a7c6e1.js +0 -0
  82. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.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
  83. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js +0 -0
  84. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js.map +0 -0
  85. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js +0 -0
  86. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js.map +0 -0
  87. {hdsp_jupyter_extension-2.0.6.dist-info → hdsp_jupyter_extension-2.0.8.dist-info}/WHEEL +0 -0
  88. {hdsp_jupyter_extension-2.0.6.dist-info → hdsp_jupyter_extension-2.0.8.dist-info}/licenses/LICENSE +0 -0
@@ -205,30 +205,42 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
205
205
  // Todo list state (from TodoListMiddleware)
206
206
  const [todos, setTodos] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)([]);
207
207
  const [isTodoExpanded, setIsTodoExpanded] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
208
+ // Agent thread ID for context persistence across cycles (SummarizationMiddleware support)
209
+ const [agentThreadId, setAgentThreadId] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);
210
+ // Rejection feedback mode - when user clicks reject, wait for optional feedback
211
+ const [isRejectionMode, setIsRejectionMode] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(false);
212
+ const [pendingRejectionInterrupt, setPendingRejectionInterrupt] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(null);
208
213
  const interruptMessageIdRef = (0,react__WEBPACK_IMPORTED_MODULE_0__.useRef)(null);
209
214
  const approvalPendingRef = (0,react__WEBPACK_IMPORTED_MODULE_0__.useRef)(false);
210
215
  const pendingToolCallsRef = (0,react__WEBPACK_IMPORTED_MODULE_0__.useRef)([]);
211
216
  const handledToolCallKeysRef = (0,react__WEBPACK_IMPORTED_MODULE_0__.useRef)(new Set());
217
+ const isNotebookWidget = (widget) => {
218
+ if (!widget)
219
+ return false;
220
+ if (widget instanceof _jupyterlab_notebook__WEBPACK_IMPORTED_MODULE_2__.NotebookPanel)
221
+ return true;
222
+ const model = widget?.content?.model;
223
+ return Boolean(model && (model.cells || model.sharedModel?.cells));
224
+ };
212
225
  const getActiveNotebookPanel = () => {
213
226
  const app = window.jupyterapp;
214
- if (app?.shell?.currentWidget) {
215
- const currentWidget = app.shell.currentWidget;
216
- if (currentWidget instanceof _jupyterlab_notebook__WEBPACK_IMPORTED_MODULE_2__.NotebookPanel) {
217
- return currentWidget;
218
- }
219
- if ('content' in currentWidget && currentWidget.content?.model) {
220
- return currentWidget;
221
- }
227
+ const currentWidget = app?.shell?.currentWidget;
228
+ if (isNotebookWidget(currentWidget)) {
229
+ return currentWidget;
222
230
  }
223
- return notebookTracker?.currentWidget || null;
231
+ if (notebookTracker?.currentWidget && isNotebookWidget(notebookTracker.currentWidget)) {
232
+ return notebookTracker.currentWidget;
233
+ }
234
+ return null;
224
235
  };
225
236
  const insertCell = (notebook, cellType, source) => {
226
237
  const model = notebook.content?.model;
227
- if (!model?.sharedModel) {
238
+ const cellCount = model?.cells?.length ?? model?.sharedModel?.cells?.length;
239
+ if (!model?.sharedModel || cellCount === undefined) {
228
240
  console.warn('[AgentPanel] Notebook model not ready for insert');
229
241
  return null;
230
242
  }
231
- const insertIndex = model.cells.length;
243
+ const insertIndex = cellCount;
232
244
  model.sharedModel.insertCell(insertIndex, {
233
245
  cell_type: cellType,
234
246
  source
@@ -236,6 +248,21 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
236
248
  notebook.content.activeCellIndex = insertIndex;
237
249
  return insertIndex;
238
250
  };
251
+ const deleteCell = (notebook, cellIndex) => {
252
+ const model = notebook.content?.model;
253
+ const cellCount = model?.cells?.length ?? model?.sharedModel?.cells?.length;
254
+ if (!model?.sharedModel || cellCount === undefined) {
255
+ console.warn('[AgentPanel] Notebook model not ready for delete');
256
+ return false;
257
+ }
258
+ if (cellIndex < 0 || cellIndex >= cellCount) {
259
+ console.warn('[AgentPanel] Invalid cell index for delete:', cellIndex);
260
+ return false;
261
+ }
262
+ model.sharedModel.deleteCell(cellIndex);
263
+ console.log('[AgentPanel] Deleted rejected cell at index:', cellIndex);
264
+ return true;
265
+ };
239
266
  const executeCell = async (notebook, cellIndex) => {
240
267
  try {
241
268
  await notebook.sessionContext.ready;
@@ -347,6 +374,7 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
347
374
  approvalPendingRef.current = pendingToolCallsRef.current.length > 0;
348
375
  return next;
349
376
  };
377
+ const getAutoApproveEnabled = (config) => (Boolean(config?.autoApprove));
350
378
  const queueApprovalCell = (code) => {
351
379
  const notebook = getActiveNotebookPanel();
352
380
  if (!notebook) {
@@ -750,6 +778,7 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
750
778
  }
751
779
  };
752
780
  // Python 에러 메시지 처리 및 파일 수정 요청
781
+ // Returns true if handled, false if should fall back to regular chat
753
782
  const handlePythonErrorFix = async (errorMessage) => {
754
783
  console.log('[AgentPanel] Handling Python error fix request');
755
784
  // 1. 에러가 발생한 파일 경로 추출
@@ -761,8 +790,8 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
761
790
  console.log('[AgentPanel] All files:', allFilePaths);
762
791
  if (!errorFilePath && !mainFilePath) {
763
792
  // 파일 경로를 찾을 수 없으면 일반 채팅으로 처리
764
- console.log('[AgentPanel] No file path found, using regular chat');
765
- return;
793
+ console.log('[AgentPanel] No file path found, falling back to regular chat stream');
794
+ return false;
766
795
  }
767
796
  // 2. 주요 파일 내용 로드
768
797
  const targetFile = errorFilePath || mainFilePath;
@@ -783,7 +812,7 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
783
812
  console.error('[AgentPanel] File fix API error:', error);
784
813
  addErrorMessage('파일 수정 요청 실패: ' + error.message);
785
814
  }
786
- return;
815
+ return true; // Handled (even if failed)
787
816
  }
788
817
  // 3. 관련 파일들 (imports) 로드
789
818
  const localImports = extractLocalImports(mainContent);
@@ -820,10 +849,12 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
820
849
  setIsLoading(true);
821
850
  const result = await apiService.fileAction(request);
822
851
  handleFileFixResponse(result.response, result.fixedFiles);
852
+ return true; // Successfully handled
823
853
  }
824
854
  catch (error) {
825
855
  console.error('[AgentPanel] File fix API error:', error);
826
856
  addErrorMessage('파일 수정 요청 실패: ' + error.message);
857
+ return true; // Handled (even if failed)
827
858
  }
828
859
  finally {
829
860
  setIsLoading(false);
@@ -972,6 +1003,231 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
972
1003
  messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
973
1004
  }
974
1005
  }, [messages, isStreaming]);
1006
+ const handleNextItemSelection = async (nextText) => {
1007
+ const trimmed = nextText.trim();
1008
+ if (!trimmed)
1009
+ return;
1010
+ // Check if there's a pending interrupt (HITL approval waiting)
1011
+ const hasPendingInterrupt = interruptData !== null;
1012
+ // Check if all todos are completed
1013
+ const allTodosCompleted = todos.length === 0 || todos.every(t => t.status === 'completed');
1014
+ // Block if there's a pending interrupt OR if agent is running with incomplete todos
1015
+ // Allow if all todos are completed even if streaming is finishing up
1016
+ if (hasPendingInterrupt || (isAgentRunning && !allTodosCompleted)) {
1017
+ showNotification('다른 작업이 진행 중입니다. 완료 후 다시 시도해주세요.', 'warning');
1018
+ return;
1019
+ }
1020
+ await sendChatMessage({
1021
+ displayContent: trimmed,
1022
+ clearInput: true
1023
+ });
1024
+ };
1025
+ const sendChatMessage = async ({ displayContent, llmPrompt, textarea, clearInput = true }) => {
1026
+ const displayContentText = displayContent || (llmPrompt ? '셀 분석 요청' : '');
1027
+ if (!displayContentText && !llmPrompt)
1028
+ return;
1029
+ // Check if API key is configured before sending
1030
+ let currentConfig = llmConfig;
1031
+ if (!currentConfig) {
1032
+ // Config not loaded yet, try to load from localStorage
1033
+ currentConfig = (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_4__.getLLMConfig)() || (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_4__.getDefaultLLMConfig)();
1034
+ setLlmConfig(currentConfig);
1035
+ }
1036
+ // Check API key using ApiKeyManager
1037
+ const hasApiKey = (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_4__.hasValidApiKey)(currentConfig);
1038
+ const autoApproveEnabled = getAutoApproveEnabled(currentConfig);
1039
+ if (!hasApiKey) {
1040
+ // Show error message and open settings
1041
+ const providerName = currentConfig?.provider || 'LLM';
1042
+ const errorMessage = {
1043
+ id: makeMessageId(),
1044
+ role: 'assistant',
1045
+ content: `API Key가 설정되지 않았습니다.\n\n${providerName === 'gemini' ? 'Gemini' : providerName === 'openai' ? 'OpenAI' : 'vLLM'} API Key를 먼저 설정해주세요.\n\n설정 버튼을 클릭하여 API Key를 입력하세요.`,
1046
+ timestamp: Date.now()
1047
+ };
1048
+ setMessages(prev => [...prev, errorMessage]);
1049
+ setShowSettings(true);
1050
+ return;
1051
+ }
1052
+ const userMessage = {
1053
+ id: makeMessageId(),
1054
+ role: 'user',
1055
+ content: displayContentText,
1056
+ timestamp: Date.now()
1057
+ };
1058
+ setMessages(prev => [...prev, userMessage]);
1059
+ if (clearInput && displayContentText) {
1060
+ setInput('');
1061
+ }
1062
+ setIsLoading(true);
1063
+ setIsStreaming(true);
1064
+ // Clear todos only when starting a new task (all completed or no todos)
1065
+ // Keep todos if there are pending/in_progress items (continuation of current task)
1066
+ const hasActiveTodos = todos.some(t => t.status === 'pending' || t.status === 'in_progress');
1067
+ if (!hasActiveTodos) {
1068
+ setTodos([]);
1069
+ }
1070
+ setDebugStatus(null);
1071
+ // Clear the data attribute and ref after using it
1072
+ if (textarea && llmPrompt) {
1073
+ textarea.removeAttribute('data-llm-prompt');
1074
+ pendingLlmPromptRef.current = null;
1075
+ }
1076
+ // Create assistant message ID for streaming updates
1077
+ const assistantMessageId = makeMessageId('assistant');
1078
+ let streamedContent = '';
1079
+ setStreamingMessageId(assistantMessageId);
1080
+ // Add empty assistant message that will be updated during streaming
1081
+ const initialAssistantMessage = {
1082
+ id: assistantMessageId,
1083
+ role: 'assistant',
1084
+ content: '',
1085
+ timestamp: Date.now()
1086
+ };
1087
+ setMessages(prev => [...prev, initialAssistantMessage]);
1088
+ try {
1089
+ // Use LLM prompt if available, otherwise use the display content
1090
+ const messageToSend = llmPrompt || displayContentText;
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;
1104
+ setMessages(prev => prev.map(msg => msg.id === assistantMessageId && isChatMessage(msg)
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
+ }
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_workspace_tool'
1161
+ || interrupt.action === 'search_notebook_cells_tool'
1162
+ || interrupt.action === 'check_resource_tool'
1163
+ || interrupt.action === 'list_files_tool'
1164
+ || interrupt.action === 'read_file_tool') {
1165
+ void handleAutoToolInterrupt(interrupt);
1166
+ return;
1167
+ }
1168
+ if (autoApproveEnabled) {
1169
+ // 자동 승인 모드일 때도 인터럽트 메시지를 생성하여 코드블럭으로 표시
1170
+ upsertInterruptMessage(interrupt, true);
1171
+ void resumeFromInterrupt(interrupt, 'approve');
1172
+ return;
1173
+ }
1174
+ if (interrupt.action === 'jupyter_cell_tool' && interrupt.args?.code) {
1175
+ const shouldQueue = shouldExecuteInNotebook(interrupt.args.code);
1176
+ if (isAutoApprovedCode(interrupt.args.code)) {
1177
+ if (shouldQueue) {
1178
+ queueApprovalCell(interrupt.args.code);
1179
+ }
1180
+ void resumeFromInterrupt(interrupt, 'approve');
1181
+ return;
1182
+ }
1183
+ if (shouldQueue) {
1184
+ queueApprovalCell(interrupt.args.code);
1185
+ }
1186
+ }
1187
+ setInterruptData(interrupt);
1188
+ upsertInterruptMessage(interrupt);
1189
+ setIsLoading(false);
1190
+ setIsStreaming(false);
1191
+ },
1192
+ // onTodos callback - update todo list UI
1193
+ (newTodos) => {
1194
+ setTodos(newTodos);
1195
+ },
1196
+ // onDebugClear callback - clear debug status
1197
+ () => {
1198
+ setDebugStatus(null);
1199
+ },
1200
+ // onToolCall callback - add cells to notebook
1201
+ handleToolCall,
1202
+ // onComplete callback - capture thread_id for context persistence
1203
+ (data) => {
1204
+ if (data.threadId) {
1205
+ setAgentThreadId(data.threadId);
1206
+ console.log('[AgentPanel] Captured agentThreadId for context persistence:', data.threadId);
1207
+ }
1208
+ },
1209
+ // threadId - pass existing thread_id to continue context
1210
+ agentThreadId || undefined);
1211
+ }
1212
+ }
1213
+ catch (error) {
1214
+ const message = error instanceof Error ? error.message : 'Failed to send message';
1215
+ setDebugStatus(`오류: ${message}`);
1216
+ // Update the assistant message with error
1217
+ setMessages(prev => prev.map(msg => msg.id === assistantMessageId && isChatMessage(msg)
1218
+ ? {
1219
+ ...msg,
1220
+ content: streamedContent + `\n\nError: ${message}`
1221
+ }
1222
+ : msg));
1223
+ }
1224
+ finally {
1225
+ setIsLoading(false);
1226
+ setIsStreaming(false);
1227
+ setStreamingMessageId(null);
1228
+ // Keep completed todos visible after the run
1229
+ }
1230
+ };
975
1231
  // Extract and store code blocks from messages, setup button listeners
976
1232
  (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
977
1233
  // Use a small delay to ensure DOM is updated after message rendering
@@ -1014,6 +1270,40 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
1014
1270
  // Use event delegation - attach single listener to container
1015
1271
  const handleContainerClick = async (e) => {
1016
1272
  const target = e.target;
1273
+ const nextItem = target.closest('.jp-next-items-item');
1274
+ if (nextItem) {
1275
+ e.stopPropagation();
1276
+ e.preventDefault();
1277
+ const subject = nextItem.querySelector('.jp-next-items-subject')?.textContent?.trim() || '';
1278
+ const description = nextItem.querySelector('.jp-next-items-description')?.textContent?.trim() || '';
1279
+ const nextText = subject && description
1280
+ ? `${subject}\n\n${description}`
1281
+ : (subject || description);
1282
+ if (nextText) {
1283
+ void handleNextItemSelection(nextText);
1284
+ }
1285
+ return;
1286
+ }
1287
+ // Handle expand/collapse button
1288
+ if (target.classList.contains('code-block-toggle') || target.closest('.code-block-toggle')) {
1289
+ const button = target.classList.contains('code-block-toggle')
1290
+ ? target
1291
+ : target.closest('.code-block-toggle');
1292
+ e.stopPropagation();
1293
+ e.preventDefault();
1294
+ const container = button.closest('.code-block-container');
1295
+ if (!container)
1296
+ return;
1297
+ const isExpanded = container.classList.toggle('is-expanded');
1298
+ button.setAttribute('aria-expanded', String(isExpanded));
1299
+ button.setAttribute('title', isExpanded ? '접기' : '전체 보기');
1300
+ button.setAttribute('aria-label', isExpanded ? '접기' : '전체 보기');
1301
+ const icon = button.querySelector('.code-block-toggle-icon');
1302
+ if (icon) {
1303
+ icon.textContent = isExpanded ? '▴' : '▾';
1304
+ }
1305
+ return;
1306
+ }
1017
1307
  // Handle copy button
1018
1308
  if (target.classList.contains('code-block-copy') || target.closest('.code-block-copy')) {
1019
1309
  const button = target.classList.contains('code-block-copy')
@@ -1570,6 +1860,12 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
1570
1860
  if (lines.length === 0) {
1571
1861
  return true;
1572
1862
  }
1863
+ if (lines.some(line => (line.startsWith('!')
1864
+ || line.startsWith('%%bash')
1865
+ || line.startsWith('%%sh')
1866
+ || line.startsWith('%%shell')))) {
1867
+ return false;
1868
+ }
1573
1869
  const disallowedPatterns = [
1574
1870
  /(^|[^=!<>])=([^=]|$)/,
1575
1871
  /\.read_[a-zA-Z0-9_]*\s*\(/,
@@ -1591,7 +1887,161 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
1591
1887
  ];
1592
1888
  return lines.every(line => allowedPatterns.some(pattern => pattern.test(line)));
1593
1889
  };
1594
- const upsertInterruptMessage = (interrupt) => {
1890
+ const userRequestedNotebookExecution = () => {
1891
+ const lastUserMessage = [...messages]
1892
+ .reverse()
1893
+ .find((msg) => isChatMessage(msg) && msg.role === 'user');
1894
+ const content = lastUserMessage?.content ?? '';
1895
+ return /노트북|셀|cell|notebook|jupyter/i.test(content);
1896
+ };
1897
+ const isShellCell = (code) => {
1898
+ const lines = code.split('\n');
1899
+ const firstLine = lines.find(line => line.trim().length > 0)?.trim() || '';
1900
+ if (firstLine.startsWith('%%bash')
1901
+ || firstLine.startsWith('%%sh')
1902
+ || firstLine.startsWith('%%shell')) {
1903
+ return true;
1904
+ }
1905
+ return lines.some(line => line.trim().startsWith('!'));
1906
+ };
1907
+ const extractShellCommand = (code) => {
1908
+ const lines = code.split('\n');
1909
+ const firstNonEmptyIndex = lines.findIndex(line => line.trim().length > 0);
1910
+ if (firstNonEmptyIndex === -1) {
1911
+ return '';
1912
+ }
1913
+ const firstLine = lines[firstNonEmptyIndex].trim();
1914
+ if (firstLine.startsWith('%%bash')
1915
+ || firstLine.startsWith('%%sh')
1916
+ || firstLine.startsWith('%%shell')) {
1917
+ const script = lines.slice(firstNonEmptyIndex + 1).join('\n').trim();
1918
+ if (!script) {
1919
+ return '';
1920
+ }
1921
+ const escaped = script
1922
+ .replace(/\\/g, '\\\\')
1923
+ .replace(/'/g, "\\'")
1924
+ .replace(/\r?\n/g, '\\n');
1925
+ return `bash -lc $'${escaped}'`;
1926
+ }
1927
+ const shellLines = lines
1928
+ .map(line => line.trim())
1929
+ .filter(line => line.startsWith('!'))
1930
+ .map(line => line.replace(/^!+/, '').trim())
1931
+ .filter(Boolean);
1932
+ return shellLines.join('\n');
1933
+ };
1934
+ const buildPythonCommand = (code) => (`python3 -c ${JSON.stringify(code)}`);
1935
+ const shouldExecuteInNotebook = (code) => {
1936
+ const notebook = getActiveNotebookPanel();
1937
+ if (!notebook) {
1938
+ return false;
1939
+ }
1940
+ if (isShellCell(code) && !userRequestedNotebookExecution()) {
1941
+ return false;
1942
+ }
1943
+ return true;
1944
+ };
1945
+ const truncateOutputLines = (output, maxLines = 2) => {
1946
+ const lines = output.split(/\r?\n/).filter(line => line.length > 0);
1947
+ const text = lines.slice(0, maxLines).join('\n');
1948
+ return { text, truncated: lines.length > maxLines };
1949
+ };
1950
+ const createCommandOutputMessage = (command) => {
1951
+ const messageId = makeMessageId('command-output');
1952
+ const outputMessage = {
1953
+ id: messageId,
1954
+ role: 'system',
1955
+ content: `🐚 ${command}\n`,
1956
+ timestamp: Date.now(),
1957
+ metadata: {
1958
+ kind: 'shell-output',
1959
+ command
1960
+ }
1961
+ };
1962
+ setMessages(prev => [...prev, outputMessage]);
1963
+ return messageId;
1964
+ };
1965
+ const appendCommandOutputMessage = (messageId, text, stream) => {
1966
+ if (!text)
1967
+ return;
1968
+ const prefix = stream === 'stderr' ? '[stderr] ' : '';
1969
+ setMessages(prev => prev.map(msg => {
1970
+ if (msg.id !== messageId || !('role' in msg)) {
1971
+ return msg;
1972
+ }
1973
+ const chatMsg = msg;
1974
+ return {
1975
+ ...chatMsg,
1976
+ content: `${chatMsg.content}${prefix}${text}`
1977
+ };
1978
+ }));
1979
+ };
1980
+ const executeSubprocessCommand = async (command, timeout, onOutput, stdin) => {
1981
+ try {
1982
+ const result = await apiService.executeCommandStream(command, { timeout, onOutput, stdin });
1983
+ const stdout = typeof result.stdout === 'string' ? result.stdout : '';
1984
+ const stderr = typeof result.stderr === 'string' ? result.stderr : '';
1985
+ const combined = [stdout, stderr].filter(Boolean).join('\n');
1986
+ const summary = truncateOutputLines(combined, 2);
1987
+ const truncated = summary.truncated || Boolean(result.truncated);
1988
+ const output = summary.text || '(no output)';
1989
+ if (result.success) {
1990
+ return {
1991
+ success: true,
1992
+ output,
1993
+ returncode: result.returncode ?? null,
1994
+ command,
1995
+ truncated
1996
+ };
1997
+ }
1998
+ const errorText = summary.text || result.error || stderr || 'Command failed';
1999
+ return {
2000
+ success: false,
2001
+ output,
2002
+ error: errorText,
2003
+ returncode: result.returncode ?? null,
2004
+ command,
2005
+ truncated
2006
+ };
2007
+ }
2008
+ catch (error) {
2009
+ const message = error instanceof Error ? error.message : 'Command execution failed';
2010
+ if (onOutput) {
2011
+ onOutput({ stream: 'stderr', text: `${message}\n` });
2012
+ }
2013
+ const summary = truncateOutputLines(message, 2);
2014
+ return {
2015
+ success: false,
2016
+ output: '',
2017
+ error: summary.text || 'Command execution failed',
2018
+ returncode: null,
2019
+ command,
2020
+ truncated: summary.truncated
2021
+ };
2022
+ }
2023
+ };
2024
+ const executeCodeViaSubprocess = async (code, timeout) => {
2025
+ const isShell = isShellCell(code);
2026
+ const command = isShell ? extractShellCommand(code) : buildPythonCommand(code);
2027
+ if (!command) {
2028
+ return {
2029
+ success: false,
2030
+ output: '',
2031
+ error: isShell ? 'Shell command is empty' : 'Python command is empty',
2032
+ returncode: null,
2033
+ command,
2034
+ truncated: false,
2035
+ execution_method: 'subprocess'
2036
+ };
2037
+ }
2038
+ const result = await executeSubprocessCommand(command, timeout);
2039
+ return {
2040
+ ...result,
2041
+ execution_method: 'subprocess'
2042
+ };
2043
+ };
2044
+ const upsertInterruptMessage = (interrupt, autoApproved) => {
1595
2045
  const interruptMessageId = interruptMessageIdRef.current || makeMessageId('interrupt');
1596
2046
  interruptMessageIdRef.current = interruptMessageId;
1597
2047
  const interruptMessage = {
@@ -1599,7 +2049,14 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
1599
2049
  role: 'system',
1600
2050
  content: interrupt.description || '코드 실행 승인이 필요합니다.',
1601
2051
  timestamp: Date.now(),
1602
- metadata: { interrupt }
2052
+ metadata: {
2053
+ interrupt: {
2054
+ ...interrupt,
2055
+ resolved: autoApproved || false,
2056
+ decision: autoApproved ? 'approve' : undefined,
2057
+ autoApproved: autoApproved || false
2058
+ }
2059
+ }
1603
2060
  };
1604
2061
  setMessages(prev => {
1605
2062
  const hasExisting = prev.some(msg => msg.id === interruptMessageId);
@@ -1609,7 +2066,7 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
1609
2066
  return [...prev, interruptMessage];
1610
2067
  });
1611
2068
  };
1612
- const clearInterruptMessage = () => {
2069
+ const clearInterruptMessage = (decision) => {
1613
2070
  if (!interruptMessageIdRef.current)
1614
2071
  return;
1615
2072
  const messageId = interruptMessageIdRef.current;
@@ -1624,7 +2081,8 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
1624
2081
  ...chatMsg.metadata,
1625
2082
  interrupt: {
1626
2083
  ...(chatMsg.metadata?.interrupt || {}),
1627
- resolved: true
2084
+ resolved: true,
2085
+ decision: decision || 'approve' // Track approve/reject decision
1628
2086
  }
1629
2087
  }
1630
2088
  };
@@ -1632,68 +2090,781 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
1632
2090
  return msg;
1633
2091
  }));
1634
2092
  };
1635
- const resumeFromInterrupt = async (interrupt, decision) => {
1636
- const { threadId } = interrupt;
1637
- setInterruptData(null);
1638
- setDebugStatus(null);
1639
- clearInterruptMessage();
1640
- let resumeDecision = decision;
1641
- let resumeArgs = undefined;
1642
- if (decision === 'approve') {
1643
- const executed = await executePendingApproval();
1644
- if (executed && executed.tool === 'jupyter_cell') {
1645
- resumeDecision = 'edit';
1646
- resumeArgs = {
1647
- code: executed.code,
1648
- execution_result: executed.execution_result
1649
- };
2093
+ const validateRelativePath = (path) => {
2094
+ const trimmed = path.trim();
2095
+ if (trimmed.startsWith('/') || trimmed.startsWith('\\') || /^[A-Za-z]:/.test(trimmed)) {
2096
+ return { valid: false, error: 'Absolute paths are not allowed' };
2097
+ }
2098
+ if (trimmed.includes('..')) {
2099
+ return { valid: false, error: 'Path traversal (..) is not allowed' };
2100
+ }
2101
+ return { valid: true };
2102
+ };
2103
+ const normalizeContentsPath = (path) => {
2104
+ const trimmed = path.trim();
2105
+ if (!trimmed || trimmed === '.' || trimmed === './') {
2106
+ return '';
2107
+ }
2108
+ return trimmed
2109
+ .replace(/^\.\/+/, '')
2110
+ .replace(/^\/+/, '')
2111
+ .replace(/\/+$/, '');
2112
+ };
2113
+ const globToRegex = (pattern) => {
2114
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
2115
+ const regex = `^${escaped.replace(/\*/g, '.*').replace(/\?/g, '.')}$`;
2116
+ return new RegExp(regex);
2117
+ };
2118
+ const fetchContentsModel = async (path, options) => {
2119
+ try {
2120
+ const { PageConfig, URLExt } = await Promise.resolve(/*! import() */).then(__webpack_require__.t.bind(__webpack_require__, /*! @jupyterlab/coreutils */ "webpack/sharing/consume/default/@jupyterlab/coreutils", 23));
2121
+ const baseUrl = PageConfig.getBaseUrl();
2122
+ const normalizedPath = normalizeContentsPath(path);
2123
+ const apiUrl = URLExt.join(baseUrl, 'api/contents', normalizedPath);
2124
+ const query = new URLSearchParams();
2125
+ if (options?.content !== undefined) {
2126
+ query.set('content', options.content ? '1' : '0');
1650
2127
  }
1651
- else if (executed && executed.tool === 'markdown') {
1652
- resumeDecision = 'edit';
1653
- resumeArgs = {
1654
- content: executed.content
1655
- };
2128
+ if (options?.format) {
2129
+ query.set('format', options.format);
2130
+ }
2131
+ const url = query.toString() ? `${apiUrl}?${query.toString()}` : apiUrl;
2132
+ const response = await fetch(url, {
2133
+ method: 'GET',
2134
+ credentials: 'include',
2135
+ });
2136
+ if (!response.ok) {
2137
+ return { success: false, error: `Failed to load contents: ${response.status}` };
1656
2138
  }
2139
+ const data = await response.json();
2140
+ return { success: true, data };
1657
2141
  }
1658
- else {
1659
- pendingToolCallsRef.current.shift();
1660
- approvalPendingRef.current = pendingToolCallsRef.current.length > 0;
2142
+ catch (error) {
2143
+ const message = error instanceof Error ? error.message : 'Failed to load contents';
2144
+ return { success: false, error: message };
1661
2145
  }
1662
- setIsLoading(true);
1663
- setIsStreaming(true);
1664
- let interrupted = false;
1665
- // 항상 새 메시지 생성 - 승인 UI 아래에 append되도록
1666
- const assistantMessageId = makeMessageId('assistant');
1667
- setStreamingMessageId(assistantMessageId);
1668
- // 메시지 추가 ( 아래에 append)
1669
- setMessages(prev => [
1670
- ...prev,
1671
- {
1672
- id: assistantMessageId,
1673
- role: 'assistant',
1674
- content: '',
1675
- timestamp: Date.now()
2146
+ };
2147
+ const executeListFilesTool = async (params) => {
2148
+ const pathCheck = validateRelativePath(params.path);
2149
+ if (!pathCheck.valid) {
2150
+ return { success: false, error: pathCheck.error };
2151
+ }
2152
+ const pattern = (params.pattern || '*').trim() || '*';
2153
+ const recursive = params.recursive ?? false;
2154
+ const matcher = globToRegex(pattern);
2155
+ const maxEntries = 500;
2156
+ const files = [];
2157
+ const pendingDirs = [normalizeContentsPath(params.path)];
2158
+ const visited = new Set();
2159
+ while (pendingDirs.length > 0 && files.length < maxEntries) {
2160
+ const dirPath = pendingDirs.shift() ?? '';
2161
+ if (visited.has(dirPath)) {
2162
+ continue;
1676
2163
  }
1677
- ]);
1678
- let streamedContent = '';
1679
- try {
1680
- await apiService.resumeAgent(threadId, resumeDecision, resumeArgs, resumeDecision === 'reject' ? 'User rejected this action' : undefined, llmConfig || undefined, (chunk) => {
1681
- streamedContent += chunk;
1682
- setMessages(prev => prev.map(msg => msg.id === assistantMessageId && isChatMessage(msg)
1683
- ? { ...msg, content: streamedContent }
1684
- : msg));
1685
- }, (status) => {
1686
- setDebugStatus(status);
2164
+ visited.add(dirPath);
2165
+ const contentsResult = await fetchContentsModel(dirPath, { content: true });
2166
+ if (!contentsResult.success) {
2167
+ return { success: false, error: contentsResult.error };
2168
+ }
2169
+ const model = contentsResult.data;
2170
+ if (!model || model.type !== 'directory' || !Array.isArray(model.content)) {
2171
+ const displayPath = dirPath || '.';
2172
+ return { success: false, error: `Not a directory: ${displayPath}` };
2173
+ }
2174
+ for (const entry of model.content) {
2175
+ if (!entry) {
2176
+ continue;
2177
+ }
2178
+ const name = entry.name || entry.path?.split('/').pop() || '';
2179
+ const entryPath = entry.path || name;
2180
+ const isDir = entry.type === 'directory';
2181
+ if (matcher.test(name)) {
2182
+ files.push({
2183
+ path: entryPath,
2184
+ isDir,
2185
+ size: isDir ? 0 : (entry.size ?? 0),
2186
+ });
2187
+ }
2188
+ if (recursive && isDir && entryPath) {
2189
+ pendingDirs.push(entryPath);
2190
+ }
2191
+ if (files.length >= maxEntries) {
2192
+ break;
2193
+ }
2194
+ }
2195
+ }
2196
+ const formatted = files.map((file) => {
2197
+ const icon = file.isDir ? '📁' : '📄';
2198
+ const sizeInfo = file.isDir ? '' : ` (${file.size} bytes)`;
2199
+ return `${icon} ${file.path}${file.isDir ? '/' : sizeInfo}`;
2200
+ }).join('\n');
2201
+ return {
2202
+ success: true,
2203
+ output: formatted || '(empty directory)',
2204
+ metadata: { count: files.length, files }
2205
+ };
2206
+ };
2207
+ const executeReadFileTool = async (params) => {
2208
+ if (!params.path) {
2209
+ return { success: false, error: 'Path is required' };
2210
+ }
2211
+ const pathCheck = validateRelativePath(params.path);
2212
+ if (!pathCheck.valid) {
2213
+ return { success: false, error: pathCheck.error };
2214
+ }
2215
+ const maxLines = typeof params.maxLines === 'number' ? params.maxLines : 1000;
2216
+ const safeMaxLines = Math.max(0, maxLines);
2217
+ const contentsResult = await fetchContentsModel(params.path, { content: true, format: 'text' });
2218
+ if (!contentsResult.success) {
2219
+ return { success: false, error: contentsResult.error };
2220
+ }
2221
+ const model = contentsResult.data;
2222
+ if (!model) {
2223
+ return { success: false, error: 'File not found' };
2224
+ }
2225
+ if (model.type === 'directory') {
2226
+ return { success: false, error: `Path is a directory: ${params.path}` };
2227
+ }
2228
+ let content = model.content ?? '';
2229
+ if (model.format === 'base64') {
2230
+ return { success: false, error: 'Binary file content is not supported' };
2231
+ }
2232
+ if (typeof content !== 'string') {
2233
+ content = JSON.stringify(content, null, 2);
2234
+ }
2235
+ const lines = content.split('\n');
2236
+ const sliced = lines.slice(0, safeMaxLines);
2237
+ return {
2238
+ success: true,
2239
+ output: sliced.join('\n'),
2240
+ metadata: {
2241
+ lineCount: sliced.length,
2242
+ truncated: lines.length > safeMaxLines
2243
+ }
2244
+ };
2245
+ };
2246
+ // Helper function for auto-approving search/file tools with execution results
2247
+ const handleAutoToolInterrupt = async (interrupt) => {
2248
+ const { threadId, action, args } = interrupt;
2249
+ console.log('[AgentPanel] Auto-approving tool:', action, args);
2250
+ try {
2251
+ let executionResult;
2252
+ if (action === 'search_workspace_tool') {
2253
+ setDebugStatus(`🔍 검색 실행 중: ${args?.pattern || ''}`);
2254
+ executionResult = await apiService.searchWorkspace({
2255
+ pattern: args?.pattern || '',
2256
+ file_types: args?.file_types || ['*.py', '*.ipynb'],
2257
+ path: args?.path || '.',
2258
+ max_results: args?.max_results || 50,
2259
+ case_sensitive: args?.case_sensitive || false
2260
+ });
2261
+ console.log('[AgentPanel] search_workspace result:', executionResult);
2262
+ }
2263
+ else if (action === 'search_notebook_cells_tool') {
2264
+ setDebugStatus(`🔍 노트북 검색 실행 중: ${args?.pattern || ''}`);
2265
+ executionResult = await apiService.searchNotebookCells({
2266
+ pattern: args?.pattern || '',
2267
+ notebook_path: args?.notebook_path,
2268
+ cell_type: args?.cell_type,
2269
+ max_results: args?.max_results || 30,
2270
+ case_sensitive: args?.case_sensitive || false
2271
+ });
2272
+ console.log('[AgentPanel] search_notebook_cells result:', executionResult);
2273
+ }
2274
+ else if (action === 'check_resource_tool') {
2275
+ const filesList = args?.files || [];
2276
+ setDebugStatus(`📊 리소스 체크 중: ${filesList.join(', ') || 'system'}`);
2277
+ executionResult = await apiService.checkResource({
2278
+ files: filesList,
2279
+ dataframes: args?.dataframes || [],
2280
+ file_size_command: args?.file_size_command || '',
2281
+ dataframe_check_code: args?.dataframe_check_code || ''
2282
+ });
2283
+ console.log('[AgentPanel] check_resource result:', executionResult);
2284
+ }
2285
+ else if (action === 'list_files_tool') {
2286
+ setDebugStatus('📂 파일 목록 조회 중...');
2287
+ const listParams = {
2288
+ path: typeof args?.path === 'string' ? args.path : '.',
2289
+ recursive: args?.recursive ?? false,
2290
+ pattern: args?.pattern ?? undefined
2291
+ };
2292
+ executionResult = await executeListFilesTool(listParams);
2293
+ console.log('[AgentPanel] list_files result:', executionResult);
2294
+ }
2295
+ else if (action === 'read_file_tool') {
2296
+ setDebugStatus('📄 파일 읽는 중...');
2297
+ const readParams = {
2298
+ path: typeof args?.path === 'string' ? args.path : '',
2299
+ encoding: typeof args?.encoding === 'string' ? args.encoding : undefined,
2300
+ maxLines: args?.max_lines ?? args?.maxLines
2301
+ };
2302
+ executionResult = await executeReadFileTool(readParams);
2303
+ console.log('[AgentPanel] read_file result:', executionResult);
2304
+ }
2305
+ else {
2306
+ console.warn('[AgentPanel] Unknown auto tool:', action);
2307
+ return;
2308
+ }
2309
+ // Resume with execution result
2310
+ const resumeArgs = {
2311
+ ...args,
2312
+ execution_result: executionResult
2313
+ };
2314
+ // Clear interrupt state (don't show approval UI for search)
2315
+ setInterruptData(null);
2316
+ // Create new assistant message for continued response
2317
+ const assistantMessageId = makeMessageId('assistant');
2318
+ setStreamingMessageId(assistantMessageId);
2319
+ setMessages(prev => [
2320
+ ...prev,
2321
+ {
2322
+ id: assistantMessageId,
2323
+ role: 'assistant',
2324
+ content: '',
2325
+ timestamp: Date.now()
2326
+ }
2327
+ ]);
2328
+ let streamedContent = '';
2329
+ let interrupted = false;
2330
+ setDebugStatus('🤔 LLM 응답 대기 중');
2331
+ await apiService.resumeAgent(threadId, 'edit', // Use 'edit' to pass execution_result in args
2332
+ resumeArgs, undefined, llmConfig || undefined, (chunk) => {
2333
+ streamedContent += chunk;
2334
+ setMessages(prev => prev.map(msg => msg.id === assistantMessageId && isChatMessage(msg)
2335
+ ? { ...msg, content: streamedContent }
2336
+ : msg));
2337
+ }, (status) => {
2338
+ setDebugStatus(status);
1687
2339
  }, (nextInterrupt) => {
1688
2340
  interrupted = true;
1689
2341
  approvalPendingRef.current = true;
2342
+ const autoApproveEnabled = getAutoApproveEnabled(llmConfig || (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_4__.getLLMConfig)() || (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_4__.getDefaultLLMConfig)());
2343
+ // Handle next interrupt (could be another search or code execution)
2344
+ if (nextInterrupt.action === 'search_workspace_tool'
2345
+ || nextInterrupt.action === 'search_notebook_cells_tool'
2346
+ || nextInterrupt.action === 'check_resource_tool'
2347
+ || nextInterrupt.action === 'list_files_tool'
2348
+ || nextInterrupt.action === 'read_file_tool') {
2349
+ void handleAutoToolInterrupt(nextInterrupt);
2350
+ return;
2351
+ }
2352
+ if (autoApproveEnabled) {
2353
+ // 자동 승인 모드일 때도 인터럽트 메시지를 생성하여 코드블럭으로 표시
2354
+ upsertInterruptMessage(nextInterrupt, true);
2355
+ void resumeFromInterrupt(nextInterrupt, 'approve');
2356
+ return;
2357
+ }
1690
2358
  if (nextInterrupt.action === 'jupyter_cell_tool' && nextInterrupt.args?.code) {
2359
+ const shouldQueue = shouldExecuteInNotebook(nextInterrupt.args.code);
1691
2360
  if (isAutoApprovedCode(nextInterrupt.args.code)) {
2361
+ if (shouldQueue) {
2362
+ queueApprovalCell(nextInterrupt.args.code);
2363
+ }
2364
+ void resumeFromInterrupt(nextInterrupt, 'approve');
2365
+ return;
2366
+ }
2367
+ if (shouldQueue) {
1692
2368
  queueApprovalCell(nextInterrupt.args.code);
2369
+ }
2370
+ }
2371
+ setInterruptData(nextInterrupt);
2372
+ upsertInterruptMessage(nextInterrupt);
2373
+ setIsLoading(false);
2374
+ setIsStreaming(false);
2375
+ }, (newTodos) => {
2376
+ setTodos(newTodos);
2377
+ }, () => {
2378
+ setDebugStatus(null);
2379
+ }, handleToolCall);
2380
+ if (!interrupted) {
2381
+ setIsLoading(false);
2382
+ setIsStreaming(false);
2383
+ setStreamingMessageId(null);
2384
+ approvalPendingRef.current = false;
2385
+ }
2386
+ }
2387
+ catch (error) {
2388
+ const message = error instanceof Error ? error.message : 'Tool execution failed';
2389
+ console.error('[AgentPanel] Auto tool error:', error);
2390
+ setDebugStatus(`오류: ${message}`);
2391
+ setIsLoading(false);
2392
+ setIsStreaming(false);
2393
+ approvalPendingRef.current = false;
2394
+ }
2395
+ };
2396
+ const resumeFromInterrupt = async (interrupt, decision, feedback // Optional feedback message for rejection
2397
+ ) => {
2398
+ const { threadId } = interrupt;
2399
+ setInterruptData(null);
2400
+ setDebugStatus(null);
2401
+ clearInterruptMessage(decision); // Pass decision to track approve/reject
2402
+ let resumeDecision = decision;
2403
+ let resumeArgs = undefined;
2404
+ if (decision === 'approve') {
2405
+ // Handle write_file_tool separately - execute file write on Jupyter server
2406
+ if (interrupt.action === 'write_file_tool') {
2407
+ try {
2408
+ setDebugStatus('📝 파일 쓰기 중...');
2409
+ const writeResult = await apiService.writeFile(interrupt.args?.path || '', interrupt.args?.content || '', {
2410
+ encoding: interrupt.args?.encoding || 'utf-8',
2411
+ overwrite: interrupt.args?.overwrite || false
2412
+ });
2413
+ console.log('[AgentPanel] write_file result:', writeResult);
2414
+ resumeDecision = 'edit';
2415
+ resumeArgs = {
2416
+ ...interrupt.args,
2417
+ execution_result: writeResult
2418
+ };
2419
+ }
2420
+ catch (error) {
2421
+ const message = error instanceof Error ? error.message : 'File write failed';
2422
+ console.error('[AgentPanel] write_file error:', error);
2423
+ resumeDecision = 'edit';
2424
+ resumeArgs = {
2425
+ ...interrupt.args,
2426
+ execution_result: { success: false, error: message }
2427
+ };
2428
+ }
2429
+ }
2430
+ else if (interrupt.action === 'edit_file_tool') {
2431
+ // Handle edit_file_tool - read file, perform replacement, write back
2432
+ try {
2433
+ setDebugStatus('✏️ 파일 수정 중...');
2434
+ const filePath = interrupt.args?.path || '';
2435
+ const oldString = interrupt.args?.old_string || '';
2436
+ const newString = interrupt.args?.new_string || '';
2437
+ const replaceAll = interrupt.args?.replace_all || false;
2438
+ // Read current file content
2439
+ const readResult = await apiService.readFile(filePath);
2440
+ if (!readResult.success || readResult.content === undefined) {
2441
+ throw new Error(readResult.error || 'Failed to read file');
2442
+ }
2443
+ const originalContent = readResult.content;
2444
+ const occurrences = (originalContent.match(new RegExp(oldString.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length;
2445
+ if (occurrences === 0) {
2446
+ throw new Error(`String not found in file: '${oldString.slice(0, 100)}...'`);
2447
+ }
2448
+ if (occurrences > 1 && !replaceAll) {
2449
+ throw new Error(`String appears ${occurrences} times. Use replace_all=True or provide more context.`);
2450
+ }
2451
+ // Perform replacement
2452
+ const newContent = replaceAll
2453
+ ? originalContent.split(oldString).join(newString)
2454
+ : originalContent.replace(oldString, newString);
2455
+ // Generate diff for logging
2456
+ const diffLines = [];
2457
+ const originalLines = originalContent.split('\n');
2458
+ const newLines = newContent.split('\n');
2459
+ diffLines.push(`--- ${filePath} (before)`);
2460
+ diffLines.push(`+++ ${filePath} (after)`);
2461
+ // Simple diff generation
2462
+ let i = 0, j = 0;
2463
+ while (i < originalLines.length || j < newLines.length) {
2464
+ if (i < originalLines.length && j < newLines.length && originalLines[i] === newLines[j]) {
2465
+ i++;
2466
+ j++;
2467
+ }
2468
+ else if (i < originalLines.length && (j >= newLines.length || originalLines[i] !== newLines[j])) {
2469
+ diffLines.push(`-${originalLines[i]}`);
2470
+ i++;
2471
+ }
2472
+ else {
2473
+ diffLines.push(`+${newLines[j]}`);
2474
+ j++;
2475
+ }
2476
+ }
2477
+ const diff = diffLines.join('\n');
2478
+ // Write the modified content
2479
+ const writeResult = await apiService.writeFile(filePath, newContent, {
2480
+ encoding: 'utf-8',
2481
+ overwrite: true
2482
+ });
2483
+ console.log('[AgentPanel] edit_file result:', writeResult);
2484
+ resumeDecision = 'edit';
2485
+ resumeArgs = {
2486
+ ...interrupt.args,
2487
+ execution_result: {
2488
+ ...writeResult,
2489
+ diff,
2490
+ occurrences,
2491
+ lines_added: newLines.length - originalLines.length > 0 ? newLines.length - originalLines.length : 0,
2492
+ lines_removed: originalLines.length - newLines.length > 0 ? originalLines.length - newLines.length : 0,
2493
+ }
2494
+ };
2495
+ }
2496
+ catch (error) {
2497
+ const message = error instanceof Error ? error.message : 'File edit failed';
2498
+ console.error('[AgentPanel] edit_file error:', error);
2499
+ resumeDecision = 'edit';
2500
+ resumeArgs = {
2501
+ ...interrupt.args,
2502
+ execution_result: { success: false, error: message }
2503
+ };
2504
+ }
2505
+ }
2506
+ else if (interrupt.action === 'multiedit_file_tool') {
2507
+ // Handle multiedit_file_tool - apply multiple edits atomically (Crush 패턴)
2508
+ try {
2509
+ setDebugStatus('✏️ 다중 파일 수정 중...');
2510
+ const filePath = interrupt.args?.path || '';
2511
+ const edits = interrupt.args?.edits || [];
2512
+ if (!filePath || edits.length === 0) {
2513
+ throw new Error('File path and edits are required');
2514
+ }
2515
+ // Read current file content
2516
+ const readResult = await apiService.readFile(filePath);
2517
+ if (!readResult.success || readResult.content === undefined) {
2518
+ throw new Error(readResult.error || 'Failed to read file');
2519
+ }
2520
+ const originalContent = readResult.content;
2521
+ let currentContent = originalContent;
2522
+ let editsApplied = 0;
2523
+ let editsFailed = 0;
2524
+ const failedEdits = [];
2525
+ // Apply edits sequentially
2526
+ for (let i = 0; i < edits.length; i++) {
2527
+ const edit = edits[i];
2528
+ const oldString = edit.old_string || '';
2529
+ const newString = edit.new_string || '';
2530
+ const replaceAll = edit.replace_all || false;
2531
+ const occurrences = (currentContent.match(new RegExp(oldString.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g')) || []).length;
2532
+ if (occurrences === 0) {
2533
+ failedEdits.push(`Edit ${i + 1}: String not found`);
2534
+ editsFailed++;
2535
+ continue;
2536
+ }
2537
+ if (occurrences > 1 && !replaceAll) {
2538
+ failedEdits.push(`Edit ${i + 1}: String appears ${occurrences} times (use replace_all)`);
2539
+ editsFailed++;
2540
+ continue;
2541
+ }
2542
+ // Apply this edit
2543
+ currentContent = replaceAll
2544
+ ? currentContent.split(oldString).join(newString)
2545
+ : currentContent.replace(oldString, newString);
2546
+ editsApplied++;
2547
+ }
2548
+ // Generate diff
2549
+ const diffLines = [];
2550
+ const originalLines = originalContent.split('\n');
2551
+ const newLines = currentContent.split('\n');
2552
+ diffLines.push(`--- ${filePath} (before)`);
2553
+ diffLines.push(`+++ ${filePath} (after)`);
2554
+ let i = 0, j = 0;
2555
+ while (i < originalLines.length || j < newLines.length) {
2556
+ if (i < originalLines.length && j < newLines.length && originalLines[i] === newLines[j]) {
2557
+ i++;
2558
+ j++;
2559
+ }
2560
+ else if (i < originalLines.length && (j >= newLines.length || originalLines[i] !== newLines[j])) {
2561
+ diffLines.push(`-${originalLines[i]}`);
2562
+ i++;
2563
+ }
2564
+ else {
2565
+ diffLines.push(`+${newLines[j]}`);
2566
+ j++;
2567
+ }
2568
+ }
2569
+ const diff = diffLines.join('\n');
2570
+ // Write the modified content if any edits succeeded
2571
+ if (editsApplied > 0) {
2572
+ const writeResult = await apiService.writeFile(filePath, currentContent, {
2573
+ encoding: 'utf-8',
2574
+ overwrite: true
2575
+ });
2576
+ console.log('[AgentPanel] multiedit_file result:', writeResult, 'applied:', editsApplied, 'failed:', editsFailed);
2577
+ resumeDecision = 'edit';
2578
+ resumeArgs = {
2579
+ ...interrupt.args,
2580
+ execution_result: {
2581
+ ...writeResult,
2582
+ diff,
2583
+ edits_applied: editsApplied,
2584
+ edits_failed: editsFailed,
2585
+ failed_details: failedEdits,
2586
+ lines_added: newLines.length - originalLines.length > 0 ? newLines.length - originalLines.length : 0,
2587
+ lines_removed: originalLines.length - newLines.length > 0 ? originalLines.length - newLines.length : 0,
2588
+ }
2589
+ };
2590
+ }
2591
+ else {
2592
+ throw new Error(`All ${edits.length} edits failed: ${failedEdits.join('; ')}`);
2593
+ }
2594
+ }
2595
+ catch (error) {
2596
+ const message = error instanceof Error ? error.message : 'Multi-edit failed';
2597
+ console.error('[AgentPanel] multiedit_file error:', error);
2598
+ resumeDecision = 'edit';
2599
+ resumeArgs = {
2600
+ ...interrupt.args,
2601
+ execution_result: { success: false, error: message, edits_applied: 0, edits_failed: 0 }
2602
+ };
2603
+ }
2604
+ }
2605
+ else if (interrupt.action === 'diagnostics_tool') {
2606
+ // Handle diagnostics_tool - get LSP diagnostics via LSP Bridge
2607
+ try {
2608
+ setDebugStatus('🔍 LSP 진단 조회 중...');
2609
+ const filePath = interrupt.args?.path || null;
2610
+ const severityFilter = interrupt.args?.severity_filter || null;
2611
+ // Get LSP Bridge instance
2612
+ const lspBridge = window._hdspLSPBridge;
2613
+ if (lspBridge && lspBridge.isLSPAvailable()) {
2614
+ const result = await lspBridge.getDiagnostics(filePath);
2615
+ // Apply severity filter if specified
2616
+ let diagnostics = result.diagnostics;
2617
+ if (severityFilter) {
2618
+ diagnostics = diagnostics.filter((d) => d.severity === severityFilter);
2619
+ }
2620
+ resumeDecision = 'approve';
2621
+ resumeArgs = {
2622
+ ...interrupt.args,
2623
+ execution_result: {
2624
+ success: true,
2625
+ lsp_available: true,
2626
+ diagnostics
2627
+ }
2628
+ };
2629
+ }
2630
+ else {
2631
+ // LSP not available - return empty result
2632
+ console.log('[AgentPanel] LSP not available, returning empty diagnostics');
2633
+ resumeDecision = 'approve';
2634
+ resumeArgs = {
2635
+ ...interrupt.args,
2636
+ execution_result: {
2637
+ success: true,
2638
+ lsp_available: false,
2639
+ diagnostics: []
2640
+ }
2641
+ };
2642
+ }
2643
+ }
2644
+ catch (error) {
2645
+ const message = error instanceof Error ? error.message : 'Diagnostics failed';
2646
+ console.error('[AgentPanel] diagnostics_tool error:', error);
2647
+ resumeDecision = 'approve';
2648
+ resumeArgs = {
2649
+ ...interrupt.args,
2650
+ execution_result: { success: false, lsp_available: false, error: message, diagnostics: [] }
2651
+ };
2652
+ }
2653
+ }
2654
+ else if (interrupt.action === 'references_tool') {
2655
+ // Handle references_tool - find symbol references
2656
+ try {
2657
+ setDebugStatus('🔗 참조 검색 중...');
2658
+ const symbol = interrupt.args?.symbol || '';
2659
+ const filePath = interrupt.args?.path || null;
2660
+ // Get LSP Bridge instance
2661
+ const lspBridge = window._hdspLSPBridge;
2662
+ if (lspBridge && lspBridge.isLSPAvailable()) {
2663
+ const result = await lspBridge.getReferences(symbol, filePath, interrupt.args?.line, interrupt.args?.character);
2664
+ resumeDecision = 'approve';
2665
+ resumeArgs = {
2666
+ ...interrupt.args,
2667
+ execution_result: {
2668
+ success: result.success,
2669
+ lsp_available: true,
2670
+ locations: result.locations,
2671
+ used_grep: false
2672
+ }
2673
+ };
2674
+ }
2675
+ else {
2676
+ // LSP not available - suggest using search_workspace_tool
2677
+ console.log('[AgentPanel] LSP not available for references, suggesting grep fallback');
2678
+ resumeDecision = 'approve';
2679
+ resumeArgs = {
2680
+ ...interrupt.args,
2681
+ execution_result: {
2682
+ success: false,
2683
+ lsp_available: false,
2684
+ locations: [],
2685
+ used_grep: false
2686
+ }
2687
+ };
2688
+ }
2689
+ }
2690
+ catch (error) {
2691
+ const message = error instanceof Error ? error.message : 'References search failed';
2692
+ console.error('[AgentPanel] references_tool error:', error);
2693
+ resumeDecision = 'approve';
2694
+ resumeArgs = {
2695
+ ...interrupt.args,
2696
+ execution_result: { success: false, lsp_available: false, error: message, locations: [] }
2697
+ };
2698
+ }
2699
+ }
2700
+ else if (interrupt.action === 'execute_command_tool') {
2701
+ const command = (interrupt.args?.command || '').trim();
2702
+ // Default stdin to "y\n" for interactive prompts (yes/no)
2703
+ const stdinInput = interrupt.args?.stdin ?? 'y\n';
2704
+ setDebugStatus('🐚 셸 명령 실행 중...');
2705
+ const outputMessageId = command ? createCommandOutputMessage(command) : null;
2706
+ const execResult = command
2707
+ ? await executeSubprocessCommand(command, interrupt.args?.timeout, outputMessageId
2708
+ ? (chunk) => appendCommandOutputMessage(outputMessageId, chunk.text, chunk.stream)
2709
+ : undefined, stdinInput)
2710
+ : {
2711
+ success: false,
2712
+ output: '',
2713
+ error: 'Command is required',
2714
+ returncode: null,
2715
+ command,
2716
+ truncated: false
2717
+ };
2718
+ resumeDecision = 'edit';
2719
+ resumeArgs = {
2720
+ ...interrupt.args,
2721
+ execution_result: execResult
2722
+ };
2723
+ }
2724
+ else if (interrupt.action === 'jupyter_cell_tool' && interrupt.args?.code) {
2725
+ const code = interrupt.args.code;
2726
+ if (!shouldExecuteInNotebook(code)) {
2727
+ setDebugStatus('🐚 서브프로세스로 코드 실행 중...');
2728
+ const execResult = await executeCodeViaSubprocess(code, interrupt.args?.timeout);
2729
+ resumeDecision = 'edit';
2730
+ resumeArgs = {
2731
+ code,
2732
+ execution_result: execResult
2733
+ };
2734
+ }
2735
+ else {
2736
+ const executed = await executePendingApproval();
2737
+ if (executed && executed.tool === 'jupyter_cell') {
2738
+ resumeDecision = 'edit';
2739
+ resumeArgs = {
2740
+ code: executed.code,
2741
+ execution_result: executed.execution_result
2742
+ };
2743
+ }
2744
+ else {
2745
+ const notebook = getActiveNotebookPanel();
2746
+ if (!notebook) {
2747
+ setDebugStatus('🐚 노트북이 없어 서브프로세스로 실행 중...');
2748
+ const execResult = await executeCodeViaSubprocess(code, interrupt.args?.timeout);
2749
+ resumeDecision = 'edit';
2750
+ resumeArgs = {
2751
+ code,
2752
+ execution_result: execResult
2753
+ };
2754
+ }
2755
+ else {
2756
+ const cellIndex = insertCell(notebook, 'code', code);
2757
+ if (cellIndex === null) {
2758
+ setDebugStatus('🐚 노트북 모델이 없어 서브프로세스로 실행 중...');
2759
+ const execResult = await executeCodeViaSubprocess(code, interrupt.args?.timeout);
2760
+ resumeDecision = 'edit';
2761
+ resumeArgs = {
2762
+ code,
2763
+ execution_result: execResult
2764
+ };
2765
+ }
2766
+ else {
2767
+ await executeCell(notebook, cellIndex);
2768
+ const execResult = captureExecutionResult(notebook, cellIndex);
2769
+ resumeDecision = 'edit';
2770
+ resumeArgs = {
2771
+ code,
2772
+ execution_result: execResult
2773
+ };
2774
+ }
2775
+ }
2776
+ }
2777
+ }
2778
+ }
2779
+ else {
2780
+ const executed = await executePendingApproval();
2781
+ if (executed && executed.tool === 'jupyter_cell') {
2782
+ resumeDecision = 'edit';
2783
+ resumeArgs = {
2784
+ code: executed.code,
2785
+ execution_result: executed.execution_result
2786
+ };
2787
+ }
2788
+ else if (executed && executed.tool === 'markdown') {
2789
+ resumeDecision = 'edit';
2790
+ resumeArgs = {
2791
+ content: executed.content
2792
+ };
2793
+ }
2794
+ }
2795
+ }
2796
+ else {
2797
+ // Reject: delete the pending cell from notebook
2798
+ const pendingCall = pendingToolCallsRef.current[0];
2799
+ if (pendingCall && pendingCall.cellIndex !== undefined) {
2800
+ const notebook = getActiveNotebookPanel();
2801
+ if (notebook) {
2802
+ deleteCell(notebook, pendingCall.cellIndex);
2803
+ }
2804
+ }
2805
+ pendingToolCallsRef.current.shift();
2806
+ approvalPendingRef.current = pendingToolCallsRef.current.length > 0;
2807
+ }
2808
+ setIsLoading(true);
2809
+ setIsStreaming(true);
2810
+ let interrupted = false;
2811
+ // 항상 새 메시지 생성 - 승인 UI 아래에 append되도록
2812
+ const assistantMessageId = makeMessageId('assistant');
2813
+ setStreamingMessageId(assistantMessageId);
2814
+ // 새 메시지 추가 (맨 아래에 append)
2815
+ setMessages(prev => [
2816
+ ...prev,
2817
+ {
2818
+ id: assistantMessageId,
2819
+ role: 'assistant',
2820
+ content: '',
2821
+ timestamp: Date.now()
2822
+ }
2823
+ ]);
2824
+ let streamedContent = '';
2825
+ // Build feedback message for rejection
2826
+ const rejectionFeedback = resumeDecision === 'reject'
2827
+ ? (feedback ? `사용자 피드백: ${feedback}` : 'User rejected this action')
2828
+ : undefined;
2829
+ try {
2830
+ await apiService.resumeAgent(threadId, resumeDecision, resumeArgs, rejectionFeedback, llmConfig || undefined, (chunk) => {
2831
+ streamedContent += chunk;
2832
+ setMessages(prev => prev.map(msg => msg.id === assistantMessageId && isChatMessage(msg)
2833
+ ? { ...msg, content: streamedContent }
2834
+ : msg));
2835
+ }, (status) => {
2836
+ setDebugStatus(status);
2837
+ }, (nextInterrupt) => {
2838
+ interrupted = true;
2839
+ approvalPendingRef.current = true;
2840
+ const autoApproveEnabled = getAutoApproveEnabled(llmConfig || (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_4__.getLLMConfig)() || (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_4__.getDefaultLLMConfig)());
2841
+ // Auto-approve search/file/resource tools
2842
+ if (nextInterrupt.action === 'search_workspace_tool'
2843
+ || nextInterrupt.action === 'search_notebook_cells_tool'
2844
+ || nextInterrupt.action === 'check_resource_tool'
2845
+ || nextInterrupt.action === 'list_files_tool'
2846
+ || nextInterrupt.action === 'read_file_tool') {
2847
+ void handleAutoToolInterrupt(nextInterrupt);
2848
+ return;
2849
+ }
2850
+ if (autoApproveEnabled) {
2851
+ // 자동 승인 모드일 때도 인터럽트 메시지를 생성하여 코드블럭으로 표시
2852
+ upsertInterruptMessage(nextInterrupt, true);
2853
+ void resumeFromInterrupt(nextInterrupt, 'approve');
2854
+ return;
2855
+ }
2856
+ if (nextInterrupt.action === 'jupyter_cell_tool' && nextInterrupt.args?.code) {
2857
+ const shouldQueue = shouldExecuteInNotebook(nextInterrupt.args.code);
2858
+ if (isAutoApprovedCode(nextInterrupt.args.code)) {
2859
+ if (shouldQueue) {
2860
+ queueApprovalCell(nextInterrupt.args.code);
2861
+ }
1693
2862
  void resumeFromInterrupt(nextInterrupt, 'approve');
1694
2863
  return;
1695
2864
  }
1696
- queueApprovalCell(nextInterrupt.args.code);
2865
+ if (shouldQueue) {
2866
+ queueApprovalCell(nextInterrupt.args.code);
2867
+ }
1697
2868
  }
1698
2869
  setInterruptData(nextInterrupt);
1699
2870
  upsertInterruptMessage(nextInterrupt);
@@ -1728,6 +2899,26 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
1728
2899
  }
1729
2900
  };
1730
2901
  const handleSendMessage = async () => {
2902
+ // Handle rejection mode - resume with optional feedback
2903
+ if (isRejectionMode && pendingRejectionInterrupt) {
2904
+ const feedback = input.trim() || undefined; // Empty input means no feedback
2905
+ // Add user message bubble if there's feedback
2906
+ if (feedback) {
2907
+ const userMessage = {
2908
+ id: makeMessageId(),
2909
+ role: 'user',
2910
+ content: feedback,
2911
+ timestamp: Date.now(),
2912
+ };
2913
+ setMessages(prev => [...prev, userMessage]);
2914
+ }
2915
+ setInput('');
2916
+ setIsRejectionMode(false);
2917
+ const interruptToResume = pendingRejectionInterrupt;
2918
+ setPendingRejectionInterrupt(null);
2919
+ await resumeFromInterrupt(interruptToResume, 'reject', feedback);
2920
+ return;
2921
+ }
1731
2922
  // Check if there's an LLM prompt stored (from cell action)
1732
2923
  const textarea = messagesEndRef.current?.parentElement?.querySelector('.jp-agent-input');
1733
2924
  const llmPrompt = pendingLlmPromptRef.current || textarea?.getAttribute('data-llm-prompt');
@@ -1751,22 +2942,24 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
1751
2942
  }
1752
2943
  // 노트북이 없는 경우
1753
2944
  if (!notebook) {
1754
- // Python 에러가 감지되면 파일 수정 모드로 전환
1755
- if (detectPythonError(currentInput)) {
1756
- console.log('[AgentPanel] Agent mode: No notebook, but Python error detected - switching to file fix mode');
1757
- // User 메시지 추가
1758
- const userMessage = {
1759
- id: makeMessageId(),
1760
- role: 'user',
1761
- content: currentInput,
1762
- timestamp: Date.now(),
1763
- };
1764
- setMessages(prev => [...prev, userMessage]);
1765
- setInput('');
1766
- // Python 에러 수정 처리
1767
- await handlePythonErrorFix(currentInput);
1768
- return;
1769
- }
2945
+ // [DISABLED] Python 에러가 감지되면 파일 수정 모드로 전환
2946
+ // 레거시 기능은 /file/action API가 구현되지 않아 비활성화됨
2947
+ // if (detectPythonError(currentInput)) {
2948
+ // console.log('[AgentPanel] Agent mode: No notebook, but Python error detected - attempting file fix');
2949
+ // const handled = await handlePythonErrorFix(currentInput);
2950
+ // if (handled) {
2951
+ // const userMessage: IChatMessage = {
2952
+ // id: makeMessageId(),
2953
+ // role: 'user',
2954
+ // content: currentInput,
2955
+ // timestamp: Date.now(),
2956
+ // };
2957
+ // setMessages(prev => [...prev, userMessage]);
2958
+ // setInput('');
2959
+ // return;
2960
+ // }
2961
+ // console.log('[AgentPanel] Agent mode: No file path found, continuing...');
2962
+ // }
1770
2963
  // 파일 수정 관련 자연어 요청 감지 (에러, 고쳐, 수정, fix 등)
1771
2964
  const fileFixRequestPatterns = [
1772
2965
  /에러.*해결/i,
@@ -1859,164 +3052,52 @@ SyntaxError: '(' was never closed
1859
3052
  return;
1860
3053
  }
1861
3054
  }
1862
- // Python 에러 감지 및 파일 수정 모드 (Chat 모드에서만)
1863
- if (inputMode === 'chat' && detectPythonError(currentInput)) {
1864
- console.log('[AgentPanel] Python error detected in message');
1865
- // User 메시지 추가
1866
- const userMessage = {
1867
- id: makeMessageId(),
1868
- role: 'user',
1869
- content: currentInput,
1870
- timestamp: Date.now(),
1871
- };
1872
- setMessages(prev => [...prev, userMessage]);
1873
- setInput('');
1874
- // Python 에러 수정 처리
1875
- await handlePythonErrorFix(currentInput);
1876
- return;
1877
- }
1878
- // Check if API key is configured before sending
1879
- let currentConfig = llmConfig;
1880
- if (!currentConfig) {
1881
- // Config not loaded yet, try to load from localStorage
1882
- currentConfig = (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_4__.getLLMConfig)() || (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_4__.getDefaultLLMConfig)();
1883
- setLlmConfig(currentConfig);
1884
- }
1885
- // Check API key using ApiKeyManager
1886
- const hasApiKey = (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_4__.hasValidApiKey)(currentConfig);
1887
- if (!hasApiKey) {
1888
- // Show error message and open settings
1889
- const providerName = llmConfig?.provider || 'LLM';
1890
- const errorMessage = {
1891
- id: makeMessageId(),
1892
- role: 'assistant',
1893
- content: `API Key가 설정되지 않았습니다.\n\n${providerName === 'gemini' ? 'Gemini' : providerName === 'openai' ? 'OpenAI' : 'vLLM'} API Key를 먼저 설정해주세요.\n\n설정 버튼을 클릭하여 API Key를 입력하세요.`,
1894
- timestamp: Date.now()
1895
- };
1896
- setMessages(prev => [...prev, errorMessage]);
1897
- setShowSettings(true);
1898
- return;
1899
- }
3055
+ // [DISABLED] Python 에러 감지 및 파일 수정 모드 (Chat 모드에서만)
3056
+ // 레거시 기능은 /file/action API가 구현되지 않아 비활성화됨
3057
+ // LangChain agent의 edit_file_tool을 사용하도록 변경됨
3058
+ // if (inputMode === 'chat' && detectPythonError(currentInput)) {
3059
+ // console.log('[AgentPanel] Python error detected in message, attempting file fix...');
3060
+ // const handled = await handlePythonErrorFix(currentInput);
3061
+ // if (handled) {
3062
+ // const userMessage: IChatMessage = {
3063
+ // id: makeMessageId(),
3064
+ // role: 'user',
3065
+ // content: currentInput,
3066
+ // timestamp: Date.now(),
3067
+ // };
3068
+ // setMessages(prev => [...prev, userMessage]);
3069
+ // setInput('');
3070
+ // return;
3071
+ // }
3072
+ // console.log('[AgentPanel] No file path found in error, using regular LLM stream');
3073
+ // }
1900
3074
  // Use the display prompt (input) for the user message, or use a fallback if input is empty
1901
3075
  const displayContent = currentInput || (llmPrompt ? '셀 분석 요청' : '');
1902
- const userMessage = {
1903
- id: makeMessageId(),
1904
- role: 'user',
1905
- content: displayContent,
1906
- timestamp: Date.now()
1907
- };
1908
- setMessages(prev => [...prev, userMessage]);
1909
- // Only clear input if it was manually entered, keep it for auto-execution display
1910
- if (currentInput) {
1911
- setInput('');
1912
- }
1913
- setIsLoading(true);
1914
- setIsStreaming(true);
1915
- // Clear the data attribute and ref after using it
1916
- if (textarea && llmPrompt) {
1917
- textarea.removeAttribute('data-llm-prompt');
1918
- pendingLlmPromptRef.current = null;
1919
- }
1920
- // Create assistant message ID for streaming updates
1921
- const assistantMessageId = makeMessageId('assistant');
1922
- let streamedContent = '';
1923
- setStreamingMessageId(assistantMessageId);
1924
- // Add empty assistant message that will be updated during streaming
1925
- const initialAssistantMessage = {
1926
- id: assistantMessageId,
1927
- role: 'assistant',
1928
- content: '',
1929
- timestamp: Date.now()
1930
- };
1931
- setMessages(prev => [...prev, initialAssistantMessage]);
1932
- let interrupted = false;
1933
- try {
1934
- // Use LLM prompt if available, otherwise use the display content
1935
- const messageToSend = llmPrompt || displayContent;
1936
- await apiService.sendMessageStream({
1937
- message: messageToSend,
1938
- conversationId: conversationId || undefined,
1939
- llmConfig: currentConfig // Include API keys with request
1940
- },
1941
- // onChunk callback - update message content incrementally
1942
- (chunk) => {
1943
- streamedContent += chunk;
1944
- setMessages(prev => prev.map(msg => msg.id === assistantMessageId && isChatMessage(msg)
1945
- ? { ...msg, content: streamedContent }
1946
- : msg));
1947
- },
1948
- // onMetadata callback - update conversationId and metadata
1949
- (metadata) => {
1950
- if (metadata.conversationId && !conversationId) {
1951
- setConversationId(metadata.conversationId);
1952
- }
1953
- if (metadata.provider || metadata.model) {
1954
- setMessages(prev => prev.map(msg => msg.id === assistantMessageId && isChatMessage(msg)
1955
- ? {
1956
- ...msg,
1957
- metadata: {
1958
- ...msg.metadata,
1959
- provider: metadata.provider,
1960
- model: metadata.model
1961
- }
1962
- }
1963
- : msg));
1964
- }
1965
- },
1966
- // onDebug callback - show debug status in gray
1967
- (status) => {
1968
- setDebugStatus(status);
1969
- },
1970
- // onInterrupt callback - show approval dialog
1971
- (interrupt) => {
1972
- interrupted = true;
1973
- approvalPendingRef.current = true;
1974
- if (interrupt.action === 'jupyter_cell_tool' && interrupt.args?.code) {
1975
- if (isAutoApprovedCode(interrupt.args.code)) {
1976
- queueApprovalCell(interrupt.args.code);
1977
- void resumeFromInterrupt(interrupt, 'approve');
1978
- return;
1979
- }
1980
- queueApprovalCell(interrupt.args.code);
1981
- }
1982
- setInterruptData(interrupt);
1983
- upsertInterruptMessage(interrupt);
1984
- setIsLoading(false);
1985
- setIsStreaming(false);
1986
- },
1987
- // onTodos callback - update todo list UI
1988
- (newTodos) => {
1989
- setTodos(newTodos);
1990
- },
1991
- // onDebugClear callback - clear debug status
1992
- () => {
1993
- setDebugStatus(null);
1994
- },
1995
- // onToolCall callback - add cells to notebook
1996
- handleToolCall);
1997
- }
1998
- catch (error) {
1999
- const message = error instanceof Error ? error.message : 'Failed to send message';
2000
- setDebugStatus(`오류: ${message}`);
2001
- // Update the assistant message with error
2002
- setMessages(prev => prev.map(msg => msg.id === assistantMessageId && isChatMessage(msg)
2003
- ? {
2004
- ...msg,
2005
- content: streamedContent + `\n\nError: ${message}`
2006
- }
2007
- : msg));
2008
- }
2009
- finally {
2010
- setIsLoading(false);
2011
- setIsStreaming(false);
2012
- setStreamingMessageId(null);
2013
- // Keep completed todos visible after the run
2014
- }
3076
+ await sendChatMessage({
3077
+ displayContent,
3078
+ llmPrompt,
3079
+ textarea,
3080
+ clearInput: Boolean(currentInput)
3081
+ });
2015
3082
  };
2016
3083
  // Handle resume after user approval/rejection
2017
3084
  const handleResumeAgent = async (decision) => {
2018
3085
  if (!interruptData)
2019
3086
  return;
3087
+ if (decision === 'reject') {
3088
+ // Enter rejection mode - wait for user feedback before resuming
3089
+ setIsRejectionMode(true);
3090
+ setPendingRejectionInterrupt(interruptData);
3091
+ // Clear the interrupt UI but keep the data for later, mark as rejected
3092
+ setInterruptData(null);
3093
+ clearInterruptMessage('reject'); // Pass 'reject' to show "거부됨"
3094
+ // Focus on input for user to provide feedback
3095
+ const textarea = document.querySelector('.jp-agent-input');
3096
+ if (textarea) {
3097
+ textarea.focus();
3098
+ }
3099
+ return;
3100
+ }
2020
3101
  await resumeFromInterrupt(interruptData, decision);
2021
3102
  };
2022
3103
  const handleKeyDown = (e) => {
@@ -2025,26 +3106,26 @@ SyntaxError: '(' was never closed
2025
3106
  e.preventDefault();
2026
3107
  handleSendMessage();
2027
3108
  }
2028
- // Shift+Tab: 모드 전환 (chat agent)
3109
+ // Shift+Tab: 모드 전환 (chat agent → agent_v2 → chat 순환)
2029
3110
  if (e.key === 'Tab' && e.shiftKey) {
2030
3111
  e.preventDefault();
2031
- setInputMode(prev => prev === 'chat' ? 'agent' : 'chat');
3112
+ setInputMode(prev => prev === 'chat' ? 'agent' : prev === 'agent' ? 'agent_v2' : 'chat');
2032
3113
  return;
2033
3114
  }
2034
3115
  // Cmd/Ctrl + . : 모드 전환 (대체 단축키)
2035
3116
  if (e.key === '.' && (e.metaKey || e.ctrlKey)) {
2036
3117
  e.preventDefault();
2037
- setInputMode(prev => prev === 'chat' ? 'agent' : 'chat');
3118
+ setInputMode(prev => prev === 'chat' ? 'agent' : prev === 'agent' ? 'agent_v2' : 'chat');
2038
3119
  }
2039
3120
  // Tab (without Shift): Agent 모드일 때 드롭다운 토글
2040
- if (e.key === 'Tab' && !e.shiftKey && inputMode === 'agent') {
3121
+ if (e.key === 'Tab' && !e.shiftKey && inputMode !== 'chat') {
2041
3122
  e.preventDefault();
2042
3123
  setShowModeDropdown(prev => !prev);
2043
3124
  }
2044
3125
  };
2045
- // 모드 토글 함수
3126
+ // 모드 토글 함수 (chat → agent → agent_v2 → chat 순환)
2046
3127
  const toggleMode = () => {
2047
- setInputMode(prev => prev === 'chat' ? 'agent' : 'chat');
3128
+ setInputMode(prev => prev === 'chat' ? 'agent' : prev === 'agent' ? 'agent_v2' : 'chat');
2048
3129
  setShowModeDropdown(false);
2049
3130
  };
2050
3131
  const clearChat = () => {
@@ -2143,9 +3224,6 @@ SyntaxError: '(' was never closed
2143
3224
  if (normalized.includes('resuming execution')) {
2144
3225
  return '승인 반영 후 실행 재개 중...';
2145
3226
  }
2146
- if (normalized.includes('tool')) {
2147
- return '도구 실행 중';
2148
- }
2149
3227
  return raw;
2150
3228
  };
2151
3229
  const getStatusText = () => {
@@ -2162,6 +3240,7 @@ SyntaxError: '(' was never closed
2162
3240
  return null;
2163
3241
  };
2164
3242
  const statusText = getStatusText();
3243
+ const hasActiveTodos = todos.some(todo => todo.status === 'pending' || todo.status === 'in_progress');
2165
3244
  return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-panel" },
2166
3245
  showSettings && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_SettingsPanel__WEBPACK_IMPORTED_MODULE_3__.SettingsPanel, { onClose: () => setShowSettings(false), onSave: handleSaveConfig, currentConfig: llmConfig || undefined })),
2167
3246
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-header" },
@@ -2189,34 +3268,88 @@ SyntaxError: '(' was never closed
2189
3268
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("p", null, "\uC548\uB155\uD558\uC138\uC694! HDSP Agent\uC785\uB2C8\uB2E4."),
2190
3269
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("p", { className: "jp-agent-empty-hint" }, inputMode === 'agent'
2191
3270
  ? '노트북 작업을 자연어로 요청하세요. 예: "데이터 시각화 해줘"'
2192
- : '메시지를 입력하거나 아래 버튼으로 Agent 모드를 선택하세요.'))) : (messages.map(msg => {
3271
+ : inputMode === 'agent_v2'
3272
+ ? 'Deep Agent 모드입니다. HITL 승인을 통해 도구 실행을 제어할 수 있습니다.'
3273
+ : '메시지를 입력하거나 아래 버튼으로 Agent 모드를 선택하세요.'))) : (messages.map(msg => {
2193
3274
  if (isChatMessage(msg)) {
2194
3275
  // 일반 Chat 메시지
2195
3276
  const isAssistant = msg.role === 'assistant';
3277
+ const isShellOutput = msg.metadata?.kind === 'shell-output';
3278
+ const interruptAction = msg.metadata?.interrupt?.action;
3279
+ const isWriteFile = interruptAction === 'write_file_tool';
3280
+ const isEditFile = interruptAction === 'edit_file_tool';
3281
+ const writePath = (isWriteFile
3282
+ && typeof msg.metadata?.interrupt?.args?.path === 'string') ? msg.metadata?.interrupt?.args?.path : '';
3283
+ const editPath = (isEditFile
3284
+ && typeof msg.metadata?.interrupt?.args?.path === 'string') ? msg.metadata?.interrupt?.args?.path : '';
3285
+ const autoApproved = msg.metadata?.interrupt?.autoApproved;
3286
+ const headerRole = msg.role === 'user'
3287
+ ? '사용자'
3288
+ : msg.role === 'system'
3289
+ ? (isShellOutput ? 'shell 실행' : (autoApproved ? '자동 승인됨' : '승인 요청'))
3290
+ : 'Agent';
2196
3291
  return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { key: msg.id, className: isAssistant
2197
3292
  ? 'jp-agent-message jp-agent-message-assistant-inline'
2198
- : `jp-agent-message jp-agent-message-${msg.role}` },
3293
+ : `jp-agent-message jp-agent-message-${msg.role}${isShellOutput ? ' jp-agent-message-shell-output' : ''}` },
2199
3294
  !isAssistant && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-message-header" },
2200
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-message-role" }, msg.role === 'user' ? '사용자' : msg.role === 'system' ? '승인 요청' : 'Agent'),
3295
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-message-role" }, headerRole),
2201
3296
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-message-time" }, new Date(msg.timestamp).toLocaleTimeString()))),
2202
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: `jp-agent-message-content${streamingMessageId === msg.id ? ' streaming' : ''}` }, msg.role === 'system' && msg.metadata?.interrupt ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-interrupt-inline" },
2203
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-interrupt-description" }, msg.content),
3297
+ 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' : ''}` },
3298
+ !msg.metadata?.interrupt?.autoApproved && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-interrupt-description" }, msg.content)),
2204
3299
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-interrupt-action" },
2205
3300
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-interrupt-action-args" }, (() => {
3301
+ const command = msg.metadata?.interrupt?.args?.command;
2206
3302
  const code = msg.metadata?.interrupt?.args?.code || msg.metadata?.interrupt?.args?.content || '';
2207
- const lines = code.split('\n');
2208
- const preview = lines.slice(0, 8).join('\n');
2209
- const suffix = lines.length > 8 ? '\n...' : '';
3303
+ // Handle edit_file_tool with diff preview
3304
+ let snippet;
3305
+ let language;
3306
+ if (isEditFile) {
3307
+ const oldStr = msg.metadata?.interrupt?.args?.old_string || '';
3308
+ const newStr = msg.metadata?.interrupt?.args?.new_string || '';
3309
+ const replaceAll = msg.metadata?.interrupt?.args?.replace_all;
3310
+ // Generate simple diff preview
3311
+ const oldPreview = oldStr.length > 500 ? oldStr.slice(0, 500) + '...' : oldStr;
3312
+ const newPreview = newStr.length > 500 ? newStr.slice(0, 500) + '...' : newStr;
3313
+ snippet = `--- ${editPath} (before)\n+++ ${editPath} (after)\n` +
3314
+ oldPreview.split('\n').map((line) => `-${line}`).join('\n') + '\n' +
3315
+ newPreview.split('\n').map((line) => `+${line}`).join('\n') +
3316
+ (replaceAll ? '\n\n(replace_all: true)' : '');
3317
+ language = 'diff';
3318
+ }
3319
+ else {
3320
+ snippet = (command || code || '(no details)');
3321
+ language = command ? 'bash' : 'python';
3322
+ }
2210
3323
  const resolved = msg.metadata?.interrupt?.resolved;
2211
- const actionHtml = resolved
2212
- ? '<div class="jp-agent-interrupt-actions jp-agent-interrupt-actions--resolved">승인됨</div>'
2213
- : `
3324
+ const decision = msg.metadata?.interrupt?.decision;
3325
+ const autoApproved = msg.metadata?.interrupt?.autoApproved;
3326
+ // 자동 승인일 때는 코드블럭 헤더에 배지가 표시되므로 actionHtml에는 표시하지 않음
3327
+ const resolvedText = autoApproved ? '' : (decision === 'reject' ? '거부됨' : '승인됨');
3328
+ const resolvedClass = autoApproved ? '' : (decision === 'reject' ? 'jp-agent-interrupt-actions--rejected' : 'jp-agent-interrupt-actions--resolved');
3329
+ const actionHtml = resolved && !autoApproved
3330
+ ? `<div class="jp-agent-interrupt-actions ${resolvedClass}">${resolvedText}</div>`
3331
+ : resolved && autoApproved
3332
+ ? '' // 자동 승인일 때는 actionHtml 비움 (코드블럭 헤더에 배지 표시)
3333
+ : `
2214
3334
  <div class="code-block-actions jp-agent-interrupt-actions">
2215
3335
  <button class="jp-agent-interrupt-approve-btn" data-action="approve">승인</button>
2216
3336
  <button class="jp-agent-interrupt-reject-btn" data-action="reject">거부</button>
2217
3337
  </div>
2218
3338
  `;
2219
- return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-RenderedHTMLCommon", style: { padding: '0 4px' }, dangerouslySetInnerHTML: { __html: (0,_utils_markdownRenderer__WEBPACK_IMPORTED_MODULE_7__.formatMarkdownToHtml)(`\n\`\`\`python\n${preview}${suffix}\n\`\`\``).replace('</div>', `${actionHtml}</div>`) }, onClick: (event) => {
3339
+ const renderedHtml = (() => {
3340
+ let html = (0,_utils_markdownRenderer__WEBPACK_IMPORTED_MODULE_7__.formatMarkdownToHtml)(`\n\`\`\`${language}\n${snippet}\n\`\`\``);
3341
+ if (isWriteFile && writePath) {
3342
+ const safePath = escapeHtml(writePath);
3343
+ html = html.replace(/<span class="code-block-language">[^<]*<\/span>/, `<span class="code-block-language jp-agent-interrupt-path">${safePath}</span>`);
3344
+ }
3345
+ if (isEditFile && editPath) {
3346
+ const safePath = escapeHtml(editPath);
3347
+ html = html.replace(/<span class="code-block-language">[^<]*<\/span>/, `<span class="code-block-language jp-agent-interrupt-path">✏️ ${safePath}</span>`);
3348
+ }
3349
+ // actionHtml이 비어있지 않을 때만 추가
3350
+ return actionHtml ? html.replace('</div>', `${actionHtml}</div>`) : html;
3351
+ })();
3352
+ return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-RenderedHTMLCommon", style: { padding: '0 4px' }, dangerouslySetInnerHTML: { __html: renderedHtml }, onClick: (event) => {
2220
3353
  const target = event.target;
2221
3354
  const action = target?.getAttribute?.('data-action');
2222
3355
  if (msg.metadata?.interrupt?.resolved) {
@@ -2276,27 +3409,33 @@ SyntaxError: '(' was never closed
2276
3409
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", { className: "jp-agent-file-fix-apply", onClick: () => applyFileFix(fix), title: `${fix.path} 파일에 수정 적용` }, "\uC801\uC6A9\uD558\uAE30"))))),
2277
3410
  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"))),
2278
3411
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { ref: messagesEndRef })),
2279
- statusText && todos.length === 0 && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: `jp-agent-message jp-agent-message-debug${statusText.startsWith('오류:') ? ' jp-agent-message-debug-error' : ''}` },
2280
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-debug-content" },
2281
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-debug-branch", "aria-hidden": "true" }),
2282
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-debug-text" }, statusText),
2283
- !statusText.startsWith('오류:') && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-debug-ellipsis", "aria-hidden": "true" }))))),
2284
- todos.length > 0 && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-todo-compact" },
3412
+ inputMode !== 'chat' && todos.length > 0 && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-todo-compact" },
2285
3413
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-todo-compact-header", onClick: () => setIsTodoExpanded(!isTodoExpanded) },
2286
3414
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-todo-compact-left" },
2287
3415
  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" },
2288
3416
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("path", { d: "M6 12l4-4-4-4" })),
2289
3417
  (() => {
2290
3418
  const currentTodo = todos.find(t => t.status === 'in_progress') || todos.find(t => t.status === 'pending');
2291
- return currentTodo ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null,
2292
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-todo-compact-spinner" }),
2293
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-todo-compact-current" }, currentTodo.content))) : (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-todo-compact-current" }, "\u2713 \uBAA8\uB4E0 \uC791\uC5C5 \uC644\uB8CC"));
3419
+ const isStillWorking = isStreaming || isLoading || isAgentRunning;
3420
+ if (currentTodo) {
3421
+ return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null,
3422
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-todo-compact-spinner" }),
3423
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-todo-compact-current" }, currentTodo.content)));
3424
+ }
3425
+ else if (isStillWorking) {
3426
+ return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null,
3427
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-todo-compact-spinner" }),
3428
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-todo-compact-current" }, "\uC791\uC5C5 \uB9C8\uBB34\uB9AC \uC911...")));
3429
+ }
3430
+ else {
3431
+ return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-todo-compact-current" }, "\u2713 \uBAA8\uB4E0 \uC791\uC5C5 \uC644\uB8CC"));
3432
+ }
2294
3433
  })()),
2295
3434
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-todo-compact-progress" },
2296
3435
  todos.filter(t => t.status === 'completed').length,
2297
3436
  "/",
2298
3437
  todos.length)),
2299
- statusText && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: `jp-agent-message jp-agent-message-debug jp-agent-message-debug--inline${statusText.startsWith('오류:') ? ' jp-agent-message-debug-error' : ''}` },
3438
+ statusText && hasActiveTodos && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: `jp-agent-message jp-agent-message-debug jp-agent-message-debug--inline${statusText.startsWith('오류:') ? ' jp-agent-message-debug-error' : ''}` },
2300
3439
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-debug-content" },
2301
3440
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-debug-branch", "aria-hidden": "true" }),
2302
3441
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-debug-text" }, statusText),
@@ -2308,21 +3447,31 @@ SyntaxError: '(' was never closed
2308
3447
  todo.status === 'in_progress' && react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-todo-item-spinner" }),
2309
3448
  todo.status === 'pending' && react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-todo-item-number" }, index + 1)),
2310
3449
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: `jp-agent-todo-item-text ${todo.status === 'completed' ? 'jp-agent-todo-item-text--done' : ''}` }, todo.content)))))))),
3450
+ statusText && !hasActiveTodos && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: `jp-agent-message jp-agent-message-debug jp-agent-message-debug--above-input${statusText.startsWith('오류:') ? ' jp-agent-message-debug-error' : ''}` },
3451
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-debug-content" },
3452
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-debug-text" }, statusText),
3453
+ !statusText.startsWith('오류:') && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-debug-ellipsis", "aria-hidden": "true" }))))),
2311
3454
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-input-container" },
2312
3455
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-input-wrapper" },
2313
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement("textarea", { className: `jp-agent-input ${inputMode === 'agent' ? 'jp-agent-input--agent-mode' : ''}`, value: input, onChange: (e) => setInput(e.target.value), onKeyDown: handleKeyDown, placeholder: inputMode === 'agent'
2314
- ? '노트북 작업을 입력하세요... (예: 데이터 시각화 해줘)'
2315
- : '메시지를 입력하세요...', rows: 3, disabled: isLoading || isAgentRunning }),
2316
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", { className: "jp-agent-send-button", onClick: handleSendMessage, disabled: !input.trim() || isLoading || isStreaming || isAgentRunning, title: "\uC804\uC1A1 (Enter)" }, isAgentRunning ? '실행 중...' : '전송')),
3456
+ 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
3457
+ ? '다른 방향 제시'
3458
+ : (inputMode === 'agent'
3459
+ ? '노트북 작업을 입력하세요... (예: 데이터 시각화 해줘)'
3460
+ : inputMode === 'agent_v2'
3461
+ ? 'Deep Agent 요청을 입력하세요... (HITL 승인 모드)'
3462
+ : '메시지를 입력하세요...'), rows: 3, disabled: isLoading || isAgentRunning }),
3463
+ 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 ? '거부' : '전송'))),
2317
3464
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-mode-bar" },
2318
3465
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-mode-toggle-container" },
2319
- 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)` },
2320
- 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' ? (
3466
+ 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)` },
3467
+ 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' ? (
3468
+ // 채팅 아이콘 (Chat 모드)
3469
+ 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' ? (
2321
3470
  // 무한대 아이콘 (Agent 모드)
2322
3471
  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" })) : (
2323
- // 채팅 아이콘 (Chat 모드)
2324
- 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" }))),
2325
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-mode-label" }, inputMode === 'agent' ? 'Agent' : 'Chat'),
3472
+ // 아이콘 (Agent V2 모드)
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", { className: "jp-agent-mode-label" }, inputMode === 'chat' ? 'Chat' : inputMode === 'agent' ? 'Agent' : 'Agent V2'),
2326
3475
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("svg", { className: "jp-agent-mode-chevron", viewBox: "0 0 16 16", fill: "currentColor", width: "12", height: "12" },
2327
3476
  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" }))),
2328
3477
  showModeDropdown && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-mode-dropdown" },
@@ -2335,7 +3484,12 @@ SyntaxError: '(' was never closed
2335
3484
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("svg", { viewBox: "0 0 16 16", fill: "currentColor", width: "14", height: "14" },
2336
3485
  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" })),
2337
3486
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null, "Agent"),
2338
- react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-mode-shortcut" }, "\uB178\uD2B8\uBD81 \uC790\uB3D9 \uC2E4\uD589"))))),
3487
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-mode-shortcut" }, "\uB178\uD2B8\uBD81 \uC790\uB3D9 \uC2E4\uD589")),
3488
+ 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); } },
3489
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("svg", { viewBox: "0 0 16 16", fill: "currentColor", width: "14", height: "14" },
3490
+ 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" })),
3491
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null, "Agent V2"),
3492
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-mode-shortcut" }, "Deep Agent (HITL)"))))),
2339
3493
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-mode-hints" },
2340
3494
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-mode-hint" }, "\u21E7Tab \uBAA8\uB4DC \uC804\uD658")))),
2341
3495
  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 }))));
@@ -2830,6 +3984,9 @@ const SettingsPanel = ({ onClose, onSave, currentConfig }) => {
2830
3984
  const [openaiApiKey, setOpenaiApiKey] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(initConfig.openai?.apiKey || '');
2831
3985
  const [openaiModel, setOpenaiModel] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(initConfig.openai?.model || 'gpt-4');
2832
3986
  const [systemPrompt, setSystemPrompt] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(initConfig.systemPrompt || '');
3987
+ const [workspaceRoot, setWorkspaceRoot] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(initConfig.workspaceRoot || '');
3988
+ const [autoApprove, setAutoApprove] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(Boolean(initConfig.autoApprove));
3989
+ const [idleTimeoutMinutes, setIdleTimeoutMinutes] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(initConfig.idleTimeoutMinutes ?? 60);
2833
3990
  // Update state when currentConfig changes
2834
3991
  (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
2835
3992
  if (currentConfig) {
@@ -2851,6 +4008,9 @@ const SettingsPanel = ({ onClose, onSave, currentConfig }) => {
2851
4008
  setOpenaiApiKey(currentConfig.openai?.apiKey || '');
2852
4009
  setOpenaiModel(currentConfig.openai?.model || 'gpt-4');
2853
4010
  setSystemPrompt(currentConfig.systemPrompt || (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_1__.getDefaultLLMConfig)().systemPrompt || '');
4011
+ setWorkspaceRoot(currentConfig.workspaceRoot || '');
4012
+ setAutoApprove(Boolean(currentConfig.autoApprove));
4013
+ setIdleTimeoutMinutes(currentConfig.idleTimeoutMinutes ?? 60);
2854
4014
  }
2855
4015
  }, [currentConfig]);
2856
4016
  // Helper: Build LLM config from state
@@ -2870,7 +4030,10 @@ const SettingsPanel = ({ onClose, onSave, currentConfig }) => {
2870
4030
  apiKey: openaiApiKey,
2871
4031
  model: openaiModel
2872
4032
  },
2873
- systemPrompt: systemPrompt && systemPrompt.trim() ? systemPrompt : undefined
4033
+ workspaceRoot: workspaceRoot.trim() ? workspaceRoot.trim() : undefined,
4034
+ systemPrompt: systemPrompt && systemPrompt.trim() ? systemPrompt : undefined,
4035
+ autoApprove,
4036
+ idleTimeoutMinutes
2874
4037
  });
2875
4038
  // Handlers for multiple API keys
2876
4039
  const handleAddKey = () => {
@@ -2964,6 +4127,11 @@ const SettingsPanel = ({ onClose, onSave, currentConfig }) => {
2964
4127
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("option", { value: "gemini" }, "Google Gemini"),
2965
4128
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("option", { value: "vllm" }, "vLLM"),
2966
4129
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("option", { value: "openai" }, "OpenAI"))),
4130
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-settings-group" },
4131
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", { className: "jp-agent-settings-label", htmlFor: "jp-agent-workspace-root" },
4132
+ "\uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uB8E8\uD2B8",
4133
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("small", { style: { fontWeight: 'normal', marginLeft: '8px', color: '#666' } }, "\uBE44\uC6B0\uBA74 \uD604\uC7AC \uB178\uD2B8\uBD81 \uD3F4\uB354 \uAE30\uC900, \uC808\uB300/\uC0C1\uB300 \uACBD\uB85C \uAC00\uB2A5")),
4134
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("input", { id: "jp-agent-workspace-root", type: "text", className: "jp-agent-settings-input", value: workspaceRoot, onChange: (e) => setWorkspaceRoot(e.target.value), placeholder: "\uC608: /Users/you/project", "data-testid": "workspace-root-input" })),
2967
4135
  provider === 'gemini' && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-settings-provider" },
2968
4136
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("h3", null, "Gemini \uC124\uC815"),
2969
4137
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-settings-group" },
@@ -3028,6 +4196,16 @@ const SettingsPanel = ({ onClose, onSave, currentConfig }) => {
3028
4196
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("option", { value: "gpt-4" }, "GPT-4"),
3029
4197
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("option", { value: "gpt-4-turbo" }, "GPT-4 Turbo"),
3030
4198
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("option", { value: "gpt-3.5-turbo" }, "GPT-3.5 Turbo"))))),
4199
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-settings-group" },
4200
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", { className: "jp-agent-settings-label" }, "\uC790\uB3D9 \uC2E4\uD589 \uC2B9\uC778"),
4201
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", { className: "jp-agent-settings-checkbox" },
4202
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("input", { type: "checkbox", checked: autoApprove, onChange: (e) => setAutoApprove(e.target.checked), "data-testid": "auto-approve-checkbox" }),
4203
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null, "\uC2B9\uC778 \uC5C6\uC774 \uBC14\uB85C \uC2E4\uD589 (\uCF54\uB4DC/\uD30C\uC77C/\uC178 \uD3EC\uD568)"))),
4204
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-settings-group" },
4205
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", { className: "jp-agent-settings-label", htmlFor: "jp-agent-idle-timeout" },
4206
+ "Idle Timeout (\uBD84)",
4207
+ 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)")),
4208
+ 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" })),
3031
4209
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-settings-group" },
3032
4210
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", { className: "jp-agent-settings-label" },
3033
4211
  "System Prompt (LangChain)",
@@ -3183,6 +4361,8 @@ __webpack_require__.r(__webpack_exports__);
3183
4361
  /* harmony import */ var _plugins_cell_buttons_plugin__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./plugins/cell-buttons-plugin */ "./lib/plugins/cell-buttons-plugin.js");
3184
4362
  /* harmony import */ var _plugins_prompt_generation_plugin__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./plugins/prompt-generation-plugin */ "./lib/plugins/prompt-generation-plugin.js");
3185
4363
  /* harmony import */ var _plugins_save_interceptor_plugin__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(/*! ./plugins/save-interceptor-plugin */ "./lib/plugins/save-interceptor-plugin.js");
4364
+ /* harmony import */ var _plugins_idle_monitor_plugin__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(/*! ./plugins/idle-monitor-plugin */ "./lib/plugins/idle-monitor-plugin.js");
4365
+ /* harmony import */ var _plugins_lsp_bridge_plugin__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(/*! ./plugins/lsp-bridge-plugin */ "./lib/plugins/lsp-bridge-plugin.js");
3186
4366
  /**
3187
4367
  * Jupyter Agent Extension Entry Point
3188
4368
  */
@@ -3191,6 +4371,8 @@ __webpack_require__.r(__webpack_exports__);
3191
4371
 
3192
4372
 
3193
4373
 
4374
+
4375
+
3194
4376
  // Import styles
3195
4377
  // import '../style/index.css';
3196
4378
  /**
@@ -3202,7 +4384,9 @@ const plugins = [
3202
4384
  _plugins_sidebar_plugin__WEBPACK_IMPORTED_MODULE_0__.sidebarPlugin,
3203
4385
  _plugins_cell_buttons_plugin__WEBPACK_IMPORTED_MODULE_1__.cellButtonsPlugin,
3204
4386
  _plugins_prompt_generation_plugin__WEBPACK_IMPORTED_MODULE_2__.promptGenerationPlugin,
3205
- _plugins_save_interceptor_plugin__WEBPACK_IMPORTED_MODULE_3__.saveInterceptorPlugin
4387
+ _plugins_save_interceptor_plugin__WEBPACK_IMPORTED_MODULE_3__.saveInterceptorPlugin,
4388
+ _plugins_idle_monitor_plugin__WEBPACK_IMPORTED_MODULE_4__.idleMonitorPlugin,
4389
+ _plugins_lsp_bridge_plugin__WEBPACK_IMPORTED_MODULE_5__.lspBridgePlugin
3206
4390
  ];
3207
4391
  /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (plugins);
3208
4392
 
@@ -4002,60 +5186,1020 @@ function showCustomPromptDialog(cell) {
4002
5186
  showNotification('질문 내용을 입력해주세요.', 'warning');
4003
5187
  return;
4004
5188
  }
4005
- dialogOverlay.remove();
4006
- // Get cell output
4007
- const cellOutput = getCellOutput(cell);
4008
- // Create display prompt: "Cell x: Custom request"
4009
- const displayPrompt = `${cellIndex}번째 셀: ${promptText}`;
4010
- // Create LLM prompt with code and output
4011
- let llmPrompt = `${promptText}\n\n셀 내용:\n\`\`\`\n${cellContent}\n\`\`\``;
4012
- if (cellOutput) {
4013
- llmPrompt += `\n\n실행 결과:\n\`\`\`\n${cellOutput}\n\`\`\``;
5189
+ dialogOverlay.remove();
5190
+ // Get cell output
5191
+ const cellOutput = getCellOutput(cell);
5192
+ // Create display prompt: "Cell x: Custom request"
5193
+ const displayPrompt = `${cellIndex}번째 셀: ${promptText}`;
5194
+ // Create LLM prompt with code and output
5195
+ let llmPrompt = `${promptText}\n\n셀 내용:\n\`\`\`\n${cellContent}\n\`\`\``;
5196
+ if (cellOutput) {
5197
+ llmPrompt += `\n\n실행 결과:\n\`\`\`\n${cellOutput}\n\`\`\``;
5198
+ }
5199
+ const agentPanel = window._hdspAgentPanel;
5200
+ if (agentPanel) {
5201
+ // Activate the sidebar panel
5202
+ const app = window.jupyterapp;
5203
+ if (app) {
5204
+ app.shell.activateById(agentPanel.id);
5205
+ }
5206
+ // Send both prompts with cell ID and cell index
5207
+ if (agentPanel.addCellActionMessage) {
5208
+ // cellIndex is 1-based (for display), convert to 0-based for array access
5209
+ const cellIndexZeroBased = cellIndex - 1;
5210
+ agentPanel.addCellActionMessage(_types__WEBPACK_IMPORTED_MODULE_1__.CellAction.CUSTOM_PROMPT, cellContent, displayPrompt, llmPrompt, cellId, cellIndexZeroBased);
5211
+ }
5212
+ }
5213
+ };
5214
+ submitBtn.addEventListener('click', handleSubmit);
5215
+ submitBtn.addEventListener('mouseenter', () => {
5216
+ submitBtn.style.background = 'rgba(25, 118, 210, 0.1)';
5217
+ });
5218
+ submitBtn.addEventListener('mouseleave', () => {
5219
+ submitBtn.style.background = 'transparent';
5220
+ });
5221
+ // Enter 키로 제출 (Shift+Enter는 줄바꿈)
5222
+ inputField?.addEventListener('keydown', (e) => {
5223
+ if (e.key === 'Enter' && !e.shiftKey) {
5224
+ e.preventDefault();
5225
+ handleSubmit();
5226
+ }
5227
+ });
5228
+ // 오버레이 클릭 시 다이얼로그 닫기
5229
+ dialogOverlay.addEventListener('click', (e) => {
5230
+ if (e.target === dialogOverlay) {
5231
+ dialogOverlay.remove();
5232
+ }
5233
+ });
5234
+ // ESC 키로 다이얼로그 닫기
5235
+ const handleEscapeKey = (e) => {
5236
+ if (e.key === 'Escape') {
5237
+ dialogOverlay.remove();
5238
+ document.removeEventListener('keydown', handleEscapeKey);
5239
+ }
5240
+ };
5241
+ document.addEventListener('keydown', handleEscapeKey);
5242
+ }
5243
+
5244
+
5245
+ /***/ },
5246
+
5247
+ /***/ "./lib/plugins/idle-monitor-plugin.js"
5248
+ /*!********************************************!*\
5249
+ !*** ./lib/plugins/idle-monitor-plugin.js ***!
5250
+ \********************************************/
5251
+ (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
5252
+
5253
+ __webpack_require__.r(__webpack_exports__);
5254
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
5255
+ /* harmony export */ getIdleMonitor: () => (/* binding */ getIdleMonitor),
5256
+ /* harmony export */ idleMonitorPlugin: () => (/* binding */ idleMonitorPlugin)
5257
+ /* harmony export */ });
5258
+ /* harmony import */ var _jupyterlab_notebook__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @jupyterlab/notebook */ "webpack/sharing/consume/default/@jupyterlab/notebook");
5259
+ /* harmony import */ var _jupyterlab_notebook__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_jupyterlab_notebook__WEBPACK_IMPORTED_MODULE_0__);
5260
+ /* harmony import */ var _jupyterlab_terminal__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! @jupyterlab/terminal */ "webpack/sharing/consume/default/@jupyterlab/terminal");
5261
+ /* harmony import */ var _jupyterlab_terminal__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(_jupyterlab_terminal__WEBPACK_IMPORTED_MODULE_1__);
5262
+ /* harmony import */ var _jupyterlab_coreutils__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! @jupyterlab/coreutils */ "webpack/sharing/consume/default/@jupyterlab/coreutils");
5263
+ /* harmony import */ var _jupyterlab_coreutils__WEBPACK_IMPORTED_MODULE_2___default = /*#__PURE__*/__webpack_require__.n(_jupyterlab_coreutils__WEBPACK_IMPORTED_MODULE_2__);
5264
+ /**
5265
+ * Idle Monitor Plugin
5266
+ *
5267
+ * Monitors user activity and triggers shutdown after idle timeout.
5268
+ * Activity is tracked by:
5269
+ * - Keyboard events
5270
+ * - Mouse events
5271
+ * - Notebook cell execution (kernel busy status)
5272
+ * - Terminal output
5273
+ */
5274
+
5275
+
5276
+
5277
+ /**
5278
+ * Plugin namespace
5279
+ */
5280
+ const PLUGIN_ID = '@hdsp-agent/idle-monitor';
5281
+ const CONFIG_STORAGE_KEY = 'hdsp-agent-llm-config';
5282
+ /**
5283
+ * Default idle timeout (in minutes)
5284
+ */
5285
+ const DEFAULT_IDLE_TIMEOUT_MINUTES = 60;
5286
+ // Check intervals - must be shorter than warning period to detect it
5287
+ const CHECK_INTERVAL_NORMAL_MS = 10 * 1000; // Check every 10 seconds (normal mode)
5288
+ const CHECK_INTERVAL_WARNING_MS = 1000; // Check every second (warning mode for countdown)
5289
+ // Warning period: show countdown before timeout (minimum 30 seconds, or 25% of timeout)
5290
+ const MIN_WARNING_PERIOD_MS = 30 * 1000; // Minimum 30 seconds warning
5291
+ /**
5292
+ * Idle Monitor Service
5293
+ */
5294
+ class IdleMonitorService {
5295
+ constructor() {
5296
+ this.checkIntervalId = null;
5297
+ this.countdownElement = null;
5298
+ this.isShuttingDown = false;
5299
+ this.notebookTracker = null;
5300
+ this.terminalTracker = null;
5301
+ this.isInWarningMode = false;
5302
+ this.isDisabled = false;
5303
+ /**
5304
+ * Throttle mousemove events to reduce performance impact
5305
+ */
5306
+ this.mouseMoveThrottleTime = 0;
5307
+ this.lastActivityTime = Date.now();
5308
+ // Initialize timeout from localStorage config
5309
+ this.idleTimeoutMinutes = this.loadTimeoutFromConfig();
5310
+ this.isDisabled = this.idleTimeoutMinutes === 0;
5311
+ this.idleTimeoutMs = this.idleTimeoutMinutes * 60 * 1000;
5312
+ // Warning period: 25% of timeout or minimum 30 seconds
5313
+ const warningPeriod = Math.max(MIN_WARNING_PERIOD_MS, this.idleTimeoutMs * 0.25);
5314
+ this.warningStartMs = this.idleTimeoutMs - warningPeriod;
5315
+ this.setupActivityListeners();
5316
+ this.setupStorageListener();
5317
+ this.createCountdownElement();
5318
+ if (!this.isDisabled) {
5319
+ this.startIdleCheck();
5320
+ }
5321
+ console.log('[IdleMonitor] Service initialized. Idle timeout:', this.idleTimeoutMinutes, 'minutes, warning at:', Math.round(this.warningStartMs / 1000), 'sec', this.isDisabled ? '(DISABLED)' : '');
5322
+ }
5323
+ /**
5324
+ * Load timeout configuration from localStorage
5325
+ */
5326
+ loadTimeoutFromConfig() {
5327
+ try {
5328
+ const stored = localStorage.getItem(CONFIG_STORAGE_KEY);
5329
+ if (stored) {
5330
+ const config = JSON.parse(stored);
5331
+ if (typeof config.idleTimeoutMinutes === 'number') {
5332
+ return config.idleTimeoutMinutes;
5333
+ }
5334
+ }
5335
+ }
5336
+ catch (e) {
5337
+ console.warn('[IdleMonitor] Failed to load config from localStorage:', e);
5338
+ }
5339
+ return DEFAULT_IDLE_TIMEOUT_MINUTES;
5340
+ }
5341
+ /**
5342
+ * Setup listener for localStorage changes (config updates from settings panel)
5343
+ */
5344
+ setupStorageListener() {
5345
+ window.addEventListener('storage', (e) => {
5346
+ if (e.key === CONFIG_STORAGE_KEY) {
5347
+ const newTimeout = this.loadTimeoutFromConfig();
5348
+ if (newTimeout !== this.idleTimeoutMinutes) {
5349
+ this.updateTimeout(newTimeout);
5350
+ }
5351
+ }
5352
+ });
5353
+ // Also listen for custom event (same-tab config changes)
5354
+ window.addEventListener('hdsp-config-updated', () => {
5355
+ const newTimeout = this.loadTimeoutFromConfig();
5356
+ if (newTimeout !== this.idleTimeoutMinutes) {
5357
+ this.updateTimeout(newTimeout);
5358
+ }
5359
+ });
5360
+ }
5361
+ /**
5362
+ * Update timeout dynamically
5363
+ */
5364
+ updateTimeout(minutes) {
5365
+ const wasDisabled = this.isDisabled;
5366
+ this.idleTimeoutMinutes = minutes;
5367
+ this.isDisabled = minutes === 0;
5368
+ this.idleTimeoutMs = minutes * 60 * 1000;
5369
+ // Warning period: 25% of timeout or minimum 30 seconds
5370
+ const warningPeriod = Math.max(MIN_WARNING_PERIOD_MS, this.idleTimeoutMs * 0.25);
5371
+ this.warningStartMs = this.idleTimeoutMs - warningPeriod;
5372
+ console.log('[IdleMonitor] Timeout updated to:', minutes, 'minutes', this.isDisabled ? '(DISABLED)' : '');
5373
+ // Stop idle check if disabled
5374
+ if (this.isDisabled) {
5375
+ this.stopIdleCheck();
5376
+ this.hideCountdown();
5377
+ }
5378
+ else if (wasDisabled) {
5379
+ // Re-enable if was disabled
5380
+ this.resetIdleTimer();
5381
+ this.startIdleCheck();
5382
+ }
5383
+ }
5384
+ /**
5385
+ * Stop idle check interval
5386
+ */
5387
+ stopIdleCheck() {
5388
+ if (this.checkIntervalId !== null) {
5389
+ clearInterval(this.checkIntervalId);
5390
+ this.checkIntervalId = null;
5391
+ }
5392
+ this.isInWarningMode = false;
5393
+ console.log('[IdleMonitor] Idle check stopped');
5394
+ }
5395
+ /**
5396
+ * Set notebook tracker for monitoring cell execution
5397
+ */
5398
+ setNotebookTracker(tracker) {
5399
+ this.notebookTracker = tracker;
5400
+ // Monitor cell execution changes
5401
+ tracker.currentChanged.connect(() => {
5402
+ this.resetIdleTimer();
5403
+ });
5404
+ // Monitor active cell changes
5405
+ tracker.activeCellChanged.connect(() => {
5406
+ this.resetIdleTimer();
5407
+ });
5408
+ console.log('[IdleMonitor] Notebook tracker connected');
5409
+ }
5410
+ /**
5411
+ * Set terminal tracker for monitoring terminal activity
5412
+ */
5413
+ setTerminalTracker(tracker) {
5414
+ this.terminalTracker = tracker;
5415
+ // Monitor terminal changes
5416
+ tracker.currentChanged.connect(() => {
5417
+ this.resetIdleTimer();
5418
+ });
5419
+ // Setup monitoring for existing terminals
5420
+ tracker.forEach(terminal => {
5421
+ this.setupTerminalMonitoring(terminal);
5422
+ });
5423
+ // Monitor new terminals
5424
+ tracker.widgetAdded.connect((_, terminal) => {
5425
+ this.setupTerminalMonitoring(terminal);
5426
+ });
5427
+ console.log('[IdleMonitor] Terminal tracker connected');
5428
+ }
5429
+ /**
5430
+ * Setup monitoring for a single terminal
5431
+ * Simply reset idle timer on any terminal output
5432
+ */
5433
+ setupTerminalMonitoring(terminal) {
5434
+ const terminalId = terminal.id || terminal.session?.name || 'unknown';
5435
+ const session = terminal.session;
5436
+ if (!session)
5437
+ return;
5438
+ // Listen for terminal output - any output resets idle timer
5439
+ session.messageReceived.connect(() => {
5440
+ this.resetIdleTimer();
5441
+ });
5442
+ console.log('[IdleMonitor] Terminal monitoring setup:', terminalId);
5443
+ }
5444
+ /**
5445
+ * Setup global activity listeners
5446
+ */
5447
+ setupActivityListeners() {
5448
+ // Keyboard events
5449
+ document.addEventListener('keydown', this.handleActivity.bind(this), true);
5450
+ document.addEventListener('keyup', this.handleActivity.bind(this), true);
5451
+ document.addEventListener('keypress', this.handleActivity.bind(this), true);
5452
+ // Mouse events
5453
+ document.addEventListener('mousedown', this.handleActivity.bind(this), true);
5454
+ document.addEventListener('mouseup', this.handleActivity.bind(this), true);
5455
+ document.addEventListener('mousemove', this.throttledMouseMove.bind(this), true);
5456
+ document.addEventListener('wheel', this.handleActivity.bind(this), true);
5457
+ document.addEventListener('click', this.handleActivity.bind(this), true);
5458
+ // Touch events (for tablet support)
5459
+ document.addEventListener('touchstart', this.handleActivity.bind(this), true);
5460
+ document.addEventListener('touchend', this.handleActivity.bind(this), true);
5461
+ console.log('[IdleMonitor] Activity listeners attached');
5462
+ }
5463
+ throttledMouseMove() {
5464
+ const now = Date.now();
5465
+ if (now - this.mouseMoveThrottleTime > 5000) { // Only update every 5 seconds for mouse move
5466
+ this.mouseMoveThrottleTime = now;
5467
+ this.handleActivity();
5468
+ }
5469
+ }
5470
+ /**
5471
+ * Handle any user activity
5472
+ */
5473
+ handleActivity() {
5474
+ this.resetIdleTimer();
5475
+ }
5476
+ /**
5477
+ * Reset the idle timer
5478
+ */
5479
+ resetIdleTimer() {
5480
+ this.lastActivityTime = Date.now();
5481
+ this.isShuttingDown = false;
5482
+ this.hideCountdown();
5483
+ // Switch back to normal mode if in warning mode
5484
+ this.switchToNormalMode();
5485
+ }
5486
+ /**
5487
+ * Create countdown display element (fixed position, top-right)
5488
+ */
5489
+ createCountdownElement() {
5490
+ // Create countdown container with fixed positioning
5491
+ this.countdownElement = document.createElement('div');
5492
+ this.countdownElement.id = 'hdsp-idle-countdown';
5493
+ this.countdownElement.className = 'hdsp-idle-countdown';
5494
+ this.countdownElement.style.cssText = `
5495
+ display: none;
5496
+ position: fixed;
5497
+ right: 20px;
5498
+ top: 10px;
5499
+ background: linear-gradient(135deg, #ff6b6b 0%, #ee5a5a 100%);
5500
+ color: white;
5501
+ padding: 8px 16px;
5502
+ border-radius: 20px;
5503
+ font-size: 14px;
5504
+ font-weight: 600;
5505
+ box-shadow: 0 4px 12px rgba(238, 90, 90, 0.5);
5506
+ z-index: 99999;
5507
+ white-space: nowrap;
5508
+ cursor: pointer;
5509
+ user-select: none;
5510
+ `;
5511
+ // Click to dismiss and reset timer
5512
+ this.countdownElement.addEventListener('click', () => {
5513
+ this.resetIdleTimer();
5514
+ console.log('[IdleMonitor] User clicked countdown - timer reset');
5515
+ });
5516
+ // Add pulse animation
5517
+ const style = document.createElement('style');
5518
+ style.id = 'hdsp-idle-countdown-style';
5519
+ style.textContent = `
5520
+ @keyframes hdsp-pulse {
5521
+ 0% { transform: scale(1); }
5522
+ 50% { transform: scale(1.03); }
5523
+ 100% { transform: scale(1); }
5524
+ }
5525
+
5526
+ .hdsp-idle-countdown {
5527
+ animation: hdsp-pulse 2s infinite;
5528
+ }
5529
+
5530
+ .hdsp-idle-countdown.warning {
5531
+ background: linear-gradient(135deg, #ffa726 0%, #fb8c00 100%) !important;
5532
+ box-shadow: 0 4px 12px rgba(251, 140, 0, 0.5) !important;
5533
+ }
5534
+
5535
+ .hdsp-idle-countdown.critical {
5536
+ background: linear-gradient(135deg, #ef5350 0%, #d32f2f 100%) !important;
5537
+ box-shadow: 0 4px 12px rgba(211, 47, 47, 0.6) !important;
5538
+ animation: hdsp-pulse-critical 0.5s infinite !important;
5539
+ }
5540
+
5541
+ @keyframes hdsp-pulse-critical {
5542
+ 0% { transform: scale(1); opacity: 1; }
5543
+ 50% { transform: scale(1.05); opacity: 0.9; }
5544
+ 100% { transform: scale(1); opacity: 1; }
5545
+ }
5546
+ `;
5547
+ // Remove existing style if any
5548
+ const existingStyle = document.getElementById('hdsp-idle-countdown-style');
5549
+ if (existingStyle) {
5550
+ existingStyle.remove();
5551
+ }
5552
+ document.head.appendChild(style);
5553
+ // Append to body for fixed positioning
5554
+ document.body.appendChild(this.countdownElement);
5555
+ console.log('[IdleMonitor] Countdown element created (fixed position)');
5556
+ }
5557
+ /**
5558
+ * Show countdown in toolbar
5559
+ */
5560
+ showCountdown(secondsRemaining) {
5561
+ console.log(`[IdleMonitor] showCountdown called: ${secondsRemaining}s remaining`);
5562
+ if (!this.countdownElement) {
5563
+ this.createCountdownElement();
5564
+ return;
5565
+ }
5566
+ this.countdownElement.style.display = 'block';
5567
+ this.countdownElement.textContent = `Idle Shutdown ${secondsRemaining}초 전`;
5568
+ // Update styling based on urgency
5569
+ this.countdownElement.classList.remove('warning', 'critical');
5570
+ if (secondsRemaining <= 10) {
5571
+ this.countdownElement.classList.add('critical');
5572
+ }
5573
+ else if (secondsRemaining <= 30) {
5574
+ this.countdownElement.classList.add('warning');
5575
+ }
5576
+ }
5577
+ /**
5578
+ * Hide countdown display
5579
+ */
5580
+ hideCountdown() {
5581
+ if (this.countdownElement) {
5582
+ this.countdownElement.style.display = 'none';
5583
+ }
5584
+ }
5585
+ /**
5586
+ * Check if any notebook cells are currently executing
5587
+ * Uses kernel status for reliability
5588
+ */
5589
+ hasRunningCells() {
5590
+ if (!this.notebookTracker)
5591
+ return false;
5592
+ // Check all open notebooks via kernel status
5593
+ const notebooks = this.notebookTracker.filter(() => true);
5594
+ for (const notebook of notebooks) {
5595
+ const session = notebook.sessionContext?.session;
5596
+ if (session) {
5597
+ const kernelStatus = session.kernel?.status;
5598
+ // Kernel is busy = cells are executing
5599
+ if (kernelStatus === 'busy') {
5600
+ return true;
5601
+ }
5602
+ }
5603
+ // Fallback: also check DOM for [*] indicator (for edge cases)
5604
+ if (notebook.content) {
5605
+ const cells = notebook.content.widgets;
5606
+ for (const cell of cells) {
5607
+ const model = cell.model;
5608
+ if (model && model.type === 'code') {
5609
+ const promptNode = cell.node.querySelector('.jp-InputPrompt');
5610
+ if (promptNode && promptNode.textContent?.includes('*')) {
5611
+ return true;
5612
+ }
5613
+ }
5614
+ }
5615
+ }
5616
+ }
5617
+ return false;
5618
+ }
5619
+ /**
5620
+ * Start idle checking interval
5621
+ * Starts in normal mode (10 min), switches to warning mode (1 sec) when needed
5622
+ */
5623
+ startIdleCheck() {
5624
+ this.checkIntervalId = window.setInterval(() => {
5625
+ this.checkIdleStatus();
5626
+ }, CHECK_INTERVAL_NORMAL_MS);
5627
+ console.log('[IdleMonitor] Started in normal mode: check every 10 seconds');
5628
+ }
5629
+ /**
5630
+ * Switch to warning mode (faster checks for countdown)
5631
+ */
5632
+ switchToWarningMode() {
5633
+ if (this.isInWarningMode)
5634
+ return;
5635
+ this.isInWarningMode = true;
5636
+ // Clear normal interval
5637
+ if (this.checkIntervalId !== null) {
5638
+ clearInterval(this.checkIntervalId);
5639
+ }
5640
+ // Start warning interval (1 second)
5641
+ this.checkIntervalId = window.setInterval(() => {
5642
+ this.checkIdleStatus();
5643
+ }, CHECK_INTERVAL_WARNING_MS);
5644
+ console.log('[IdleMonitor] Switched to warning mode: check every', CHECK_INTERVAL_WARNING_MS / 1000, 'seconds');
5645
+ }
5646
+ /**
5647
+ * Switch back to normal mode
5648
+ */
5649
+ switchToNormalMode() {
5650
+ if (!this.isInWarningMode)
5651
+ return;
5652
+ this.isInWarningMode = false;
5653
+ // Clear warning interval
5654
+ if (this.checkIntervalId !== null) {
5655
+ clearInterval(this.checkIntervalId);
5656
+ }
5657
+ // Start normal interval (10 minutes)
5658
+ this.checkIntervalId = window.setInterval(() => {
5659
+ this.checkIdleStatus();
5660
+ }, CHECK_INTERVAL_NORMAL_MS);
5661
+ console.log('[IdleMonitor] Switched to normal mode: check every 10 seconds');
5662
+ }
5663
+ /**
5664
+ * Check current idle status
5665
+ */
5666
+ checkIdleStatus() {
5667
+ // Skip if disabled
5668
+ if (this.isDisabled) {
5669
+ return;
5670
+ }
5671
+ // Skip if notebook cells are running (not idle)
5672
+ if (this.hasRunningCells()) {
5673
+ this.resetIdleTimer();
5674
+ return;
5675
+ }
5676
+ const now = Date.now();
5677
+ const idleTime = now - this.lastActivityTime;
5678
+ const remainingTime = this.idleTimeoutMs - idleTime;
5679
+ const secondsRemaining = Math.ceil(remainingTime / 1000);
5680
+ // Debug log every 10 seconds
5681
+ if (Math.floor(idleTime / 1000) % 10 === 0) {
5682
+ 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`);
5683
+ }
5684
+ // Enter warning period - switch to fast checking for countdown
5685
+ if (idleTime >= this.warningStartMs && !this.isShuttingDown) {
5686
+ this.switchToWarningMode();
5687
+ this.showCountdown(secondsRemaining);
5688
+ }
5689
+ // Trigger shutdown if idle timeout reached
5690
+ if (idleTime >= this.idleTimeoutMs && !this.isShuttingDown) {
5691
+ this.isShuttingDown = true;
5692
+ this.triggerShutdown();
5693
+ }
5694
+ }
5695
+ /**
5696
+ * Trigger shutdown process
5697
+ */
5698
+ async triggerShutdown() {
5699
+ console.log('[IdleMonitor] Idle timeout reached. Triggering shutdown...');
5700
+ // Hide countdown
5701
+ this.hideCountdown();
5702
+ // Show alert popup
5703
+ alert(`${this.idleTimeoutMinutes}분 동안 활동이 없어 세션이 종료됩니다.`);
5704
+ // Call shutdown API
5705
+ await this.callShutdownApi();
5706
+ }
5707
+ /**
5708
+ * Call HDSP shutdown API via Jupyter server endpoint
5709
+ * Server-side call to avoid CORS issues
5710
+ */
5711
+ async callShutdownApi() {
5712
+ try {
5713
+ // Get base URL from PageConfig
5714
+ const baseUrl = _jupyterlab_coreutils__WEBPACK_IMPORTED_MODULE_2__.PageConfig.getBaseUrl();
5715
+ const apiUrl = `${baseUrl}hdsp-agent/idle-shutdown`;
5716
+ console.log('[IdleMonitor] Calling shutdown API via server:', apiUrl);
5717
+ // Call server endpoint (which will call HDSP API)
5718
+ const response = await fetch(apiUrl, {
5719
+ method: 'POST',
5720
+ headers: {
5721
+ 'Content-Type': 'application/json'
5722
+ }
5723
+ });
5724
+ const result = await response.json();
5725
+ if (response.ok && result.success) {
5726
+ console.log('[IdleMonitor] Shutdown API call successful:', result.message);
5727
+ }
5728
+ else {
5729
+ console.error('[IdleMonitor] Shutdown API call failed:', result.error || response.statusText);
5730
+ }
5731
+ }
5732
+ catch (error) {
5733
+ console.error('[IdleMonitor] Shutdown API call error:', error);
5734
+ }
5735
+ }
5736
+ /**
5737
+ * Get current idle time in milliseconds
5738
+ */
5739
+ getIdleTimeMs() {
5740
+ return Date.now() - this.lastActivityTime;
5741
+ }
5742
+ /**
5743
+ * Manually trigger activity (for external use)
5744
+ */
5745
+ triggerActivity() {
5746
+ this.resetIdleTimer();
5747
+ }
5748
+ /**
5749
+ * Cleanup service
5750
+ */
5751
+ dispose() {
5752
+ if (this.checkIntervalId !== null) {
5753
+ clearInterval(this.checkIntervalId);
5754
+ this.checkIntervalId = null;
5755
+ }
5756
+ if (this.countdownElement && this.countdownElement.parentNode) {
5757
+ this.countdownElement.parentNode.removeChild(this.countdownElement);
5758
+ }
5759
+ console.log('[IdleMonitor] Service disposed');
5760
+ }
5761
+ }
5762
+ /**
5763
+ * Global idle monitor instance
5764
+ */
5765
+ let idleMonitor = null;
5766
+ /**
5767
+ * Get idle monitor instance
5768
+ */
5769
+ function getIdleMonitor() {
5770
+ return idleMonitor;
5771
+ }
5772
+ /**
5773
+ * Idle Monitor Plugin
5774
+ */
5775
+ const idleMonitorPlugin = {
5776
+ id: PLUGIN_ID,
5777
+ autoStart: true,
5778
+ requires: [],
5779
+ optional: [_jupyterlab_notebook__WEBPACK_IMPORTED_MODULE_0__.INotebookTracker, _jupyterlab_terminal__WEBPACK_IMPORTED_MODULE_1__.ITerminalTracker],
5780
+ activate: (app, notebookTracker, terminalTracker) => {
5781
+ console.log('[IdleMonitorPlugin] Activating Idle Monitor');
5782
+ try {
5783
+ // Create idle monitor service
5784
+ idleMonitor = new IdleMonitorService();
5785
+ // Connect notebook tracker if available
5786
+ if (notebookTracker) {
5787
+ idleMonitor.setNotebookTracker(notebookTracker);
5788
+ }
5789
+ // Connect terminal tracker if available
5790
+ if (terminalTracker) {
5791
+ idleMonitor.setTerminalTracker(terminalTracker);
5792
+ }
5793
+ // Store reference globally for debugging
5794
+ window._hdspIdleMonitor = idleMonitor;
5795
+ console.log('[IdleMonitorPlugin] Idle Monitor activated successfully');
5796
+ }
5797
+ catch (error) {
5798
+ console.error('[IdleMonitorPlugin] Failed to activate:', error);
5799
+ }
5800
+ }
5801
+ };
5802
+
5803
+
5804
+ /***/ },
5805
+
5806
+ /***/ "./lib/plugins/lsp-bridge-plugin.js"
5807
+ /*!******************************************!*\
5808
+ !*** ./lib/plugins/lsp-bridge-plugin.js ***!
5809
+ \******************************************/
5810
+ (__unused_webpack_module, __webpack_exports__, __webpack_require__) {
5811
+
5812
+ __webpack_require__.r(__webpack_exports__);
5813
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
5814
+ /* harmony export */ DiagnosticSeverity: () => (/* binding */ DiagnosticSeverity),
5815
+ /* harmony export */ "default": () => (__WEBPACK_DEFAULT_EXPORT__),
5816
+ /* harmony export */ getLSPBridge: () => (/* binding */ getLSPBridge),
5817
+ /* harmony export */ lspBridgePlugin: () => (/* binding */ lspBridgePlugin)
5818
+ /* harmony export */ });
5819
+ /* harmony import */ var _jupyterlab_notebook__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @jupyterlab/notebook */ "webpack/sharing/consume/default/@jupyterlab/notebook");
5820
+ /* harmony import */ var _jupyterlab_notebook__WEBPACK_IMPORTED_MODULE_0___default = /*#__PURE__*/__webpack_require__.n(_jupyterlab_notebook__WEBPACK_IMPORTED_MODULE_0__);
5821
+ /**
5822
+ * LSP Bridge Plugin
5823
+ *
5824
+ * jupyterlab-lsp와 HDSP Agent를 연결하는 브릿지
5825
+ * Crush 패턴 적용: Version-based 캐싱, 이벤트 기반 업데이트
5826
+ *
5827
+ * 주요 기능:
5828
+ * - LSP 진단 결과 캐싱 및 조회
5829
+ * - 심볼 참조 검색
5830
+ * - Agent 도구와 연동
5831
+ */
5832
+
5833
+ /**
5834
+ * Plugin namespace
5835
+ */
5836
+ const PLUGIN_ID = '@hdsp-agent/lsp-bridge';
5837
+ /**
5838
+ * LSP Diagnostic severity levels
5839
+ */
5840
+ var DiagnosticSeverity;
5841
+ (function (DiagnosticSeverity) {
5842
+ DiagnosticSeverity[DiagnosticSeverity["Error"] = 1] = "Error";
5843
+ DiagnosticSeverity[DiagnosticSeverity["Warning"] = 2] = "Warning";
5844
+ DiagnosticSeverity[DiagnosticSeverity["Information"] = 3] = "Information";
5845
+ DiagnosticSeverity[DiagnosticSeverity["Hint"] = 4] = "Hint";
5846
+ })(DiagnosticSeverity || (DiagnosticSeverity = {}));
5847
+ /**
5848
+ * LSP State Cache (Crush의 VersionedMap 패턴)
5849
+ * 버전 기반 캐시 무효화로 불필요한 재계산 방지
5850
+ */
5851
+ class LSPStateCache {
5852
+ constructor() {
5853
+ this.diagnosticsVersion = 0;
5854
+ this.diagnosticsCache = new Map();
5855
+ this.summaryCache = null;
5856
+ this.summaryVersion = -1;
5857
+ }
5858
+ /**
5859
+ * Update diagnostics for a file
5860
+ */
5861
+ updateDiagnostics(uri, diagnostics) {
5862
+ this.diagnosticsCache.set(uri, diagnostics);
5863
+ this.diagnosticsVersion++;
5864
+ }
5865
+ /**
5866
+ * Get diagnostics for a specific file or all files
5867
+ */
5868
+ getDiagnostics(uri) {
5869
+ if (uri) {
5870
+ return {
5871
+ version: this.diagnosticsVersion,
5872
+ diagnostics: this.diagnosticsCache.get(uri) || []
5873
+ };
5874
+ }
5875
+ return {
5876
+ version: this.diagnosticsVersion,
5877
+ diagnostics: this.diagnosticsCache
5878
+ };
5879
+ }
5880
+ /**
5881
+ * Get diagnostic counts with caching (Crush 패턴)
5882
+ */
5883
+ getDiagnosticSummary() {
5884
+ // Return cached if version matches
5885
+ if (this.summaryVersion === this.diagnosticsVersion && this.summaryCache) {
5886
+ return this.summaryCache;
5887
+ }
5888
+ // Recalculate
5889
+ let errors = 0;
5890
+ let warnings = 0;
5891
+ let hints = 0;
5892
+ for (const diags of this.diagnosticsCache.values()) {
5893
+ for (const d of diags) {
5894
+ switch (d.severity) {
5895
+ case DiagnosticSeverity.Error:
5896
+ errors++;
5897
+ break;
5898
+ case DiagnosticSeverity.Warning:
5899
+ warnings++;
5900
+ break;
5901
+ default:
5902
+ hints++;
5903
+ }
5904
+ }
5905
+ }
5906
+ this.summaryCache = {
5907
+ errors,
5908
+ warnings,
5909
+ hints,
5910
+ total: errors + warnings + hints,
5911
+ version: this.diagnosticsVersion
5912
+ };
5913
+ this.summaryVersion = this.diagnosticsVersion;
5914
+ return this.summaryCache;
5915
+ }
5916
+ /**
5917
+ * Clear diagnostics for a file
5918
+ */
5919
+ clearDiagnostics(uri) {
5920
+ if (this.diagnosticsCache.has(uri)) {
5921
+ this.diagnosticsCache.delete(uri);
5922
+ this.diagnosticsVersion++;
5923
+ }
5924
+ }
5925
+ /**
5926
+ * Clear all diagnostics
5927
+ */
5928
+ clearAll() {
5929
+ this.diagnosticsCache.clear();
5930
+ this.diagnosticsVersion++;
5931
+ }
5932
+ /**
5933
+ * Get current version
5934
+ */
5935
+ getVersion() {
5936
+ return this.diagnosticsVersion;
5937
+ }
5938
+ }
5939
+ /**
5940
+ * LSP Bridge Service
5941
+ * Agent 도구에서 LSP 기능을 사용할 수 있게 해주는 서비스
5942
+ */
5943
+ class LSPBridgeService {
5944
+ constructor() {
5945
+ this.cache = new LSPStateCache();
5946
+ this.lspAvailable = false;
5947
+ this.notebookTracker = null;
5948
+ this.checkLSPAvailability();
5949
+ }
5950
+ /**
5951
+ * Check if jupyterlab-lsp is available
5952
+ */
5953
+ async checkLSPAvailability() {
5954
+ try {
5955
+ // jupyterlab-lsp가 설치되어 있는지 확인
5956
+ // 실제로는 ILSPDocumentConnectionManager 토큰을 통해 확인
5957
+ this.lspAvailable = false; // 기본값, 실제 연결 시 true로 변경
5958
+ console.log('[LSPBridge] LSP availability check - will be updated when connection established');
5959
+ }
5960
+ catch (error) {
5961
+ console.warn('[LSPBridge] LSP not available:', error);
5962
+ this.lspAvailable = false;
4014
5963
  }
4015
- const agentPanel = window._hdspAgentPanel;
4016
- if (agentPanel) {
4017
- // Activate the sidebar panel
4018
- const app = window.jupyterapp;
4019
- if (app) {
4020
- app.shell.activateById(agentPanel.id);
5964
+ }
5965
+ /**
5966
+ * Set notebook tracker for document access
5967
+ */
5968
+ setNotebookTracker(tracker) {
5969
+ this.notebookTracker = tracker;
5970
+ }
5971
+ /**
5972
+ * Mark LSP as available (called when connection is established)
5973
+ */
5974
+ setLSPAvailable(available) {
5975
+ this.lspAvailable = available;
5976
+ console.log('[LSPBridge] LSP available:', available);
5977
+ }
5978
+ /**
5979
+ * Check if LSP is available
5980
+ */
5981
+ isLSPAvailable() {
5982
+ return this.lspAvailable;
5983
+ }
5984
+ /**
5985
+ * Update diagnostics from external source (e.g., jupyterlab-lsp)
5986
+ */
5987
+ updateDiagnostics(uri, diagnostics) {
5988
+ this.cache.updateDiagnostics(uri, diagnostics);
5989
+ this.notifyDiagnosticsChanged(uri);
5990
+ }
5991
+ /**
5992
+ * Get diagnostics for Agent tool
5993
+ * Returns formatted diagnostics for a file or entire project
5994
+ */
5995
+ async getDiagnostics(filePath) {
5996
+ const result = this.cache.getDiagnostics(filePath ? this.pathToUri(filePath) : undefined);
5997
+ const summary = this.cache.getDiagnosticSummary();
5998
+ const formattedDiagnostics = [];
5999
+ if (filePath) {
6000
+ // Single file
6001
+ const diags = result.diagnostics;
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: filePath
6011
+ });
4021
6012
  }
4022
- // Send both prompts with cell ID and cell index
4023
- if (agentPanel.addCellActionMessage) {
4024
- // cellIndex is 1-based (for display), convert to 0-based for array access
4025
- const cellIndexZeroBased = cellIndex - 1;
4026
- agentPanel.addCellActionMessage(_types__WEBPACK_IMPORTED_MODULE_1__.CellAction.CUSTOM_PROMPT, cellContent, displayPrompt, llmPrompt, cellId, cellIndexZeroBased);
6013
+ }
6014
+ else {
6015
+ // All files
6016
+ const diagsMap = result.diagnostics;
6017
+ for (const [uri, diags] of diagsMap) {
6018
+ const file = this.uriToPath(uri);
6019
+ for (const d of diags) {
6020
+ formattedDiagnostics.push({
6021
+ severity: this.severityToString(d.severity),
6022
+ line: d.range.start.line + 1,
6023
+ character: d.range.start.character,
6024
+ message: d.message,
6025
+ source: d.source,
6026
+ code: d.code,
6027
+ file
6028
+ });
6029
+ }
4027
6030
  }
4028
6031
  }
4029
- };
4030
- submitBtn.addEventListener('click', handleSubmit);
4031
- submitBtn.addEventListener('mouseenter', () => {
4032
- submitBtn.style.background = 'rgba(25, 118, 210, 0.1)';
4033
- });
4034
- submitBtn.addEventListener('mouseleave', () => {
4035
- submitBtn.style.background = 'transparent';
4036
- });
4037
- // Enter 키로 제출 (Shift+Enter는 줄바꿈)
4038
- inputField?.addEventListener('keydown', (e) => {
4039
- if (e.key === 'Enter' && !e.shiftKey) {
4040
- e.preventDefault();
4041
- handleSubmit();
6032
+ // Sort: errors first, then by file and line (Crush 패턴)
6033
+ formattedDiagnostics.sort((a, b) => {
6034
+ const severityOrder = { error: 0, warning: 1, information: 2, hint: 3 };
6035
+ const severityDiff = (severityOrder[a.severity] || 3) - (severityOrder[b.severity] || 3);
6036
+ if (severityDiff !== 0)
6037
+ return severityDiff;
6038
+ const fileDiff = a.file.localeCompare(b.file);
6039
+ if (fileDiff !== 0)
6040
+ return fileDiff;
6041
+ return a.line - b.line;
6042
+ });
6043
+ return {
6044
+ success: true,
6045
+ diagnostics: formattedDiagnostics,
6046
+ summary
6047
+ };
6048
+ }
6049
+ /**
6050
+ * Find references for a symbol (Crush의 Grep-then-LSP 패턴)
6051
+ * 현재는 grep 기반으로 구현, LSP 연결 시 향상 가능
6052
+ */
6053
+ async getReferences(symbol, filePath, line, character) {
6054
+ // LSP가 없으면 grep 기반으로 검색하도록 안내
6055
+ if (!this.lspAvailable) {
6056
+ return {
6057
+ success: false,
6058
+ locations: []
6059
+ };
4042
6060
  }
4043
- });
4044
- // 오버레이 클릭 다이얼로그 닫기
4045
- dialogOverlay.addEventListener('click', (e) => {
4046
- if (e.target === dialogOverlay) {
4047
- dialogOverlay.remove();
6061
+ // TODO: jupyterlab-lsp 연결 시 구현
6062
+ // 현재는 결과 반환 (Agent가 search_workspace_tool 사용하도록)
6063
+ return {
6064
+ success: false,
6065
+ locations: []
6066
+ };
6067
+ }
6068
+ /**
6069
+ * Get diagnostic summary
6070
+ */
6071
+ getDiagnosticSummary() {
6072
+ return this.cache.getDiagnosticSummary();
6073
+ }
6074
+ /**
6075
+ * Notify diagnostics changed event
6076
+ */
6077
+ notifyDiagnosticsChanged(uri) {
6078
+ const summary = this.cache.getDiagnosticSummary();
6079
+ window.dispatchEvent(new CustomEvent('hdsp-lsp-diagnostics-changed', {
6080
+ detail: { uri, summary }
6081
+ }));
6082
+ }
6083
+ /**
6084
+ * Convert severity number to string
6085
+ */
6086
+ severityToString(severity) {
6087
+ switch (severity) {
6088
+ case DiagnosticSeverity.Error:
6089
+ return 'error';
6090
+ case DiagnosticSeverity.Warning:
6091
+ return 'warning';
6092
+ case DiagnosticSeverity.Information:
6093
+ return 'information';
6094
+ case DiagnosticSeverity.Hint:
6095
+ return 'hint';
6096
+ default:
6097
+ return 'hint';
4048
6098
  }
4049
- });
4050
- // ESC 키로 다이얼로그 닫기
4051
- const handleEscapeKey = (e) => {
4052
- if (e.key === 'Escape') {
4053
- dialogOverlay.remove();
4054
- document.removeEventListener('keydown', handleEscapeKey);
6099
+ }
6100
+ /**
6101
+ * Convert file path to URI
6102
+ */
6103
+ pathToUri(path) {
6104
+ if (path.startsWith('file://')) {
6105
+ return path;
4055
6106
  }
4056
- };
4057
- document.addEventListener('keydown', handleEscapeKey);
6107
+ return `file://${path.startsWith('/') ? '' : '/'}${path}`;
6108
+ }
6109
+ /**
6110
+ * Convert URI to file path
6111
+ */
6112
+ uriToPath(uri) {
6113
+ if (uri.startsWith('file://')) {
6114
+ return uri.replace('file://', '');
6115
+ }
6116
+ return uri;
6117
+ }
6118
+ /**
6119
+ * Dispose service
6120
+ */
6121
+ dispose() {
6122
+ this.cache.clearAll();
6123
+ console.log('[LSPBridge] Service disposed');
6124
+ }
6125
+ }
6126
+ /**
6127
+ * Global LSP Bridge instance
6128
+ */
6129
+ let lspBridge = null;
6130
+ /**
6131
+ * Get LSP Bridge instance
6132
+ */
6133
+ function getLSPBridge() {
6134
+ return lspBridge;
6135
+ }
6136
+ /**
6137
+ * LSP Bridge Plugin
6138
+ *
6139
+ * jupyterlab-lsp가 설치되어 있으면 연동하고,
6140
+ * 없으면 기본 기능만 제공
6141
+ */
6142
+ const lspBridgePlugin = {
6143
+ id: PLUGIN_ID,
6144
+ autoStart: true,
6145
+ requires: [],
6146
+ optional: [_jupyterlab_notebook__WEBPACK_IMPORTED_MODULE_0__.INotebookTracker],
6147
+ activate: (app, notebookTracker) => {
6148
+ console.log('[LSPBridgePlugin] Activating LSP Bridge...');
6149
+ // Create LSP Bridge service
6150
+ lspBridge = new LSPBridgeService();
6151
+ // Connect notebook tracker if available
6152
+ if (notebookTracker) {
6153
+ lspBridge.setNotebookTracker(notebookTracker);
6154
+ }
6155
+ // Store reference globally for debugging and Agent access
6156
+ window._hdspLSPBridge = lspBridge;
6157
+ // Try to connect to jupyterlab-lsp if available
6158
+ // This is done dynamically to avoid hard dependency
6159
+ tryConnectToLSP(app, lspBridge);
6160
+ console.log('[LSPBridgePlugin] LSP Bridge activated');
6161
+ }
6162
+ };
6163
+ /**
6164
+ * Try to connect to jupyterlab-lsp extension
6165
+ */
6166
+ async function tryConnectToLSP(app, bridge) {
6167
+ try {
6168
+ // Check if ILSPDocumentConnectionManager is available
6169
+ // This token is provided by jupyterlab-lsp
6170
+ const lspToken = '@jupyterlab/lsp:ILSPDocumentConnectionManager';
6171
+ // Try to get the LSP manager from the application
6172
+ // Note: This requires jupyterlab-lsp to be installed
6173
+ const hasLSP = app.commands.hasCommand('lsp:show-diagnostics-panel');
6174
+ if (hasLSP) {
6175
+ console.log('[LSPBridgePlugin] jupyterlab-lsp detected');
6176
+ bridge.setLSPAvailable(true);
6177
+ // Listen for LSP diagnostic events
6178
+ // jupyterlab-lsp publishes diagnostics through its own mechanism
6179
+ setupLSPEventListeners(bridge);
6180
+ }
6181
+ else {
6182
+ console.log('[LSPBridgePlugin] jupyterlab-lsp not detected, using fallback mode');
6183
+ bridge.setLSPAvailable(false);
6184
+ }
6185
+ }
6186
+ catch (error) {
6187
+ console.warn('[LSPBridgePlugin] Failed to connect to jupyterlab-lsp:', error);
6188
+ bridge.setLSPAvailable(false);
6189
+ }
6190
+ }
6191
+ /**
6192
+ * Setup event listeners for LSP events
6193
+ */
6194
+ function setupLSPEventListeners(bridge) {
6195
+ // jupyterlab-lsp uses its own event system
6196
+ // We'll listen for changes through polling or direct integration
6197
+ // This is a placeholder for future implementation
6198
+ console.log('[LSPBridgePlugin] LSP event listeners setup (placeholder)');
6199
+ // For now, we can use a simple polling mechanism or
6200
+ // hook into JupyterLab's document change events
4058
6201
  }
6202
+ /* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (lspBridgePlugin);
4059
6203
 
4060
6204
 
4061
6205
  /***/ },
@@ -6908,40 +9052,71 @@ __webpack_require__.r(__webpack_exports__);
6908
9052
  */
6909
9053
  const STORAGE_KEY = 'hdsp-agent-llm-config';
6910
9054
  const DEFAULT_LANGCHAIN_SYSTEM_PROMPT = `You are an expert Python data scientist and Jupyter notebook assistant.
6911
- Your role is to help users with data analysis, visualization, and Python coding tasks in Jupyter notebooks.
9055
+ Your role is to help users with data analysis, visualization, and Python coding tasks in Jupyter notebooks. You can use only Korean
6912
9056
 
6913
- ## ⚠️ CRITICAL RULE: NEVER produce an empty response
9057
+ # Core Behavior
9058
+ Be concise and direct. Answer in fewer than 4 lines unless the user asks for detail.
9059
+ After working on a file, just stop - don't explain what you did unless asked.
9060
+ Avoid unnecessary introductions or conclusions.
9061
+
9062
+ ## Task Management
9063
+ Use write_todos for complex multi-step tasks (3+ steps). Mark tasks in_progress before starting, completed immediately after finishing.
9064
+ For simple 1-2 step tasks, just do them directly without todos.
6914
9065
 
6915
9066
  You MUST ALWAYS call a tool in every response. After any tool result, you MUST:
6916
9067
  1. Check your todo list - are there pending or in_progress items?
6917
9068
  2. If YES → call the next appropriate tool (jupyter_cell_tool, markdown_tool, etc.)
6918
- 3. If ALL todos are completed call final_answer_tool with a summary
6919
-
6920
- NEVER end your turn without calling a tool. NEVER produce an empty response.
9069
+ 3. When you suggest next steps for todo item '다음 단계 제시', you MUST create next steps in json format matching this schema:
9070
+ {
9071
+ "next_items": [
9072
+ {
9073
+ "subject": "<subject for next step>",
9074
+ "description": "<detailed description for the next step>"
9075
+ }, ...
9076
+ ]
9077
+ }
9078
+ 4. If ALL todos are completed → call final_answer_tool with a summary
6921
9079
 
6922
- ## Available Tools
6923
- 1. **jupyter_cell_tool**: Execute Python code in a new notebook cell
6924
- 2. **markdown_tool**: Add a markdown explanation cell
6925
- 3. **final_answer_tool**: Complete the task with a summary - REQUIRED when done
6926
- 4. **read_file_tool**: Read file contents
6927
- 5. **write_file_tool**: Write file contents
6928
- 6. **list_files_tool**: List directory contents
6929
- 7. **search_workspace_tool**: Search for patterns in workspace files
6930
- 8. **search_notebook_cells_tool**: Search for patterns in notebook cells
6931
- 9. **write_todos**: Create and update task list for complex multi-step tasks
9080
+ ## 🔴 MANDATORY: Resource Check Before Data Hanlding
9081
+ **ALWAYS call check_resource_tool FIRST** when the task involves:
9082
+ - Loading files: .csv, .parquet, .json, .xlsx, .pickle, .h5, .feather
9083
+ - Handling datasets(dataframe) with pandas, polars, dask, or similar libraries
9084
+ - Training ML models on data files
6932
9085
 
6933
9086
  ## Mandatory Workflow
6934
9087
  1. After EVERY tool result, immediately call the next tool
6935
9088
  2. Continue until ALL todos show status: "completed"
6936
9089
  3. ONLY THEN call final_answer_tool to summarize
6937
- 4. If \`!pip install\` fails, use \`!pip3 install\` instead
6938
- 5. For plots and charts, use English text only
9090
+ 4. Only use jupyter_cell_tool for Python code or when the user explicitly asks to run in a notebook cell
9091
+ 5. For plots and charts, use English text only.
6939
9092
 
6940
9093
  ## ❌ FORBIDDEN (will break the workflow)
6941
9094
  - Producing an empty response (no tool call, no content)
6942
9095
  - Stopping after any tool without calling the next tool
6943
9096
  - Ending without calling final_answer_tool
6944
9097
  - Leaving todos in "in_progress" or "pending" state without continuing
9098
+
9099
+ ## 📖 File Reading Best Practices
9100
+ **CRITICAL**: When exploring codebases or reading files, use pagination to prevent context overflow.
9101
+
9102
+ **Pattern for codebase exploration:**
9103
+ 1. First scan: read_file_tool(path, limit=100) - See file structure and key sections
9104
+ 2. Targeted read: read_file_tool(path, offset=100, limit=200) - Read specific sections if needed
9105
+ 3. Full read: Only read without limit when necessary for immediate editing
9106
+
9107
+ **When to paginate (use offset/limit):**
9108
+ - Reading any file >500 lines
9109
+ - Exploring unfamiliar codebases (always start with limit=100)
9110
+ - Reading multiple files in sequence
9111
+ - Any research or investigation task
9112
+
9113
+ **When full read is OK:**
9114
+ - Small files (<500 lines)
9115
+ - Files you need to edit immediately after reading
9116
+ - After confirming file size with first scan
9117
+
9118
+ ## 🔧 Code Development
9119
+ 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.
6945
9120
  `;
6946
9121
  // ═══════════════════════════════════════════════════════════════════════════
6947
9122
  // Key Rotation State (in-memory, not persisted)
@@ -6975,6 +9150,8 @@ function saveLLMConfig(config) {
6975
9150
  try {
6976
9151
  localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
6977
9152
  console.log('[ApiKeyManager] Config saved to localStorage');
9153
+ // Dispatch custom event for same-tab listeners (e.g., idle monitor)
9154
+ window.dispatchEvent(new CustomEvent('hdsp-config-updated', { detail: config }));
6978
9155
  }
6979
9156
  catch (e) {
6980
9157
  console.error('[ApiKeyManager] Failed to save config:', e);
@@ -7027,7 +9204,8 @@ function getDefaultLLMConfig() {
7027
9204
  endpoint: 'http://localhost:8000',
7028
9205
  model: 'default'
7029
9206
  },
7030
- systemPrompt: DEFAULT_LANGCHAIN_SYSTEM_PROMPT
9207
+ systemPrompt: DEFAULT_LANGCHAIN_SYSTEM_PROMPT,
9208
+ autoApprove: false
7031
9209
  };
7032
9210
  }
7033
9211
  /**
@@ -7254,6 +9432,8 @@ __webpack_require__.r(__webpack_exports__);
7254
9432
  class ApiService {
7255
9433
  // 생성자에서 baseUrl을 선택적으로 받도록 하되, 없으면 자동으로 계산
7256
9434
  constructor(baseUrl) {
9435
+ this.resourceUsageCache = null;
9436
+ this.resourceUsageCacheMs = 15000;
7257
9437
  if (baseUrl) {
7258
9438
  this.baseUrl = baseUrl;
7259
9439
  }
@@ -7293,6 +9473,35 @@ class ApiService {
7293
9473
  'X-XSRFToken': this.getCsrfToken()
7294
9474
  };
7295
9475
  }
9476
+ async getResourceUsageSnapshot() {
9477
+ const now = Date.now();
9478
+ if (this.resourceUsageCache
9479
+ && now - this.resourceUsageCache.timestamp < this.resourceUsageCacheMs) {
9480
+ return this.resourceUsageCache.resource;
9481
+ }
9482
+ try {
9483
+ const response = await fetch(`${this.baseUrl}/resource-usage`, {
9484
+ method: 'GET',
9485
+ credentials: 'include'
9486
+ });
9487
+ if (!response.ok) {
9488
+ return null;
9489
+ }
9490
+ const payload = await response.json().catch(() => ({}));
9491
+ const candidate = payload?.resource ?? payload;
9492
+ const snapshot = candidate && typeof candidate === 'object' && !Array.isArray(candidate)
9493
+ ? candidate
9494
+ : null;
9495
+ if (snapshot) {
9496
+ this.resourceUsageCache = { resource: snapshot, timestamp: now };
9497
+ }
9498
+ return snapshot;
9499
+ }
9500
+ catch (error) {
9501
+ console.warn('[ApiService] Resource usage fetch failed:', error);
9502
+ return null;
9503
+ }
9504
+ }
7296
9505
  // ═══════════════════════════════════════════════════════════════════════════
7297
9506
  // Global Rate Limit Handling with Key Rotation
7298
9507
  // ═══════════════════════════════════════════════════════════════════════════
@@ -7371,37 +9580,134 @@ class ApiService {
7371
9580
  }
7372
9581
  throw new Error('모든 API 키가 Rate Limit 상태입니다. 잠시 후 다시 시도해주세요.');
7373
9582
  }
7374
- // Not a rate limit error, throw immediately
7375
- throw error;
9583
+ // Not a rate limit error, throw immediately
9584
+ throw error;
9585
+ }
9586
+ }
9587
+ throw lastError || new Error('Maximum retry attempts exceeded');
9588
+ }
9589
+ /**
9590
+ * Execute cell action (explain, fix, custom)
9591
+ * Uses global rate limit handling with key rotation
9592
+ */
9593
+ async cellAction(request) {
9594
+ console.log('[ApiService] cellAction request:', request);
9595
+ return this.fetchWithKeyRotation(`${this.baseUrl}/cell/action`, request, { defaultErrorMessage: '셀 액션 실패' });
9596
+ }
9597
+ /**
9598
+ * Send chat message (non-streaming)
9599
+ * Uses global rate limit handling with key rotation
9600
+ */
9601
+ async sendMessage(request) {
9602
+ console.log('[ApiService] sendMessage request');
9603
+ return this.fetchWithKeyRotation(`${this.baseUrl}/chat/message`, request, { defaultErrorMessage: '메시지 전송 실패' });
9604
+ }
9605
+ /**
9606
+ * Send chat message with streaming response
9607
+ *
9608
+ * NOTE (Financial Security Compliance):
9609
+ * - API key rotation is handled by frontend (not server)
9610
+ * - Server receives ONLY ONE key per request
9611
+ * - On 429 rate limit, frontend rotates key and retries with next key
9612
+ */
9613
+ /**
9614
+ * 단순 Chat용 스트리밍 - /chat/stream 사용
9615
+ * 원래 main 브랜치의 구현 복원 - LangChain 에이전트 없이 단순 Q&A
9616
+ */
9617
+ async sendChatStream(request, onChunk, onMetadata) {
9618
+ const MAX_RETRIES = 10;
9619
+ let currentConfig = request.llmConfig;
9620
+ let lastError = null;
9621
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
9622
+ const requestToSend = currentConfig
9623
+ ? { ...request, llmConfig: (0,_ApiKeyManager__WEBPACK_IMPORTED_MODULE_0__.buildSingleKeyConfig)(currentConfig) }
9624
+ : request;
9625
+ try {
9626
+ await this.sendChatStreamInternal(requestToSend, onChunk, onMetadata);
9627
+ (0,_ApiKeyManager__WEBPACK_IMPORTED_MODULE_0__.resetKeyRotation)();
9628
+ return;
9629
+ }
9630
+ catch (error) {
9631
+ const errorMsg = error instanceof Error ? error.message : String(error);
9632
+ lastError = error instanceof Error ? error : new Error(errorMsg);
9633
+ if ((0,_ApiKeyManager__WEBPACK_IMPORTED_MODULE_0__.isRateLimitError)(errorMsg) && request.llmConfig) {
9634
+ console.log(`[ApiService] Chat rate limit on attempt ${attempt + 1}, trying next key...`);
9635
+ const rotatedConfig = (0,_ApiKeyManager__WEBPACK_IMPORTED_MODULE_0__.handleRateLimitError)(request.llmConfig);
9636
+ if (rotatedConfig) {
9637
+ currentConfig = request.llmConfig;
9638
+ continue;
9639
+ }
9640
+ else {
9641
+ throw new Error('모든 API 키가 Rate Limit 상태입니다. 잠시 후 다시 시도해주세요.');
9642
+ }
9643
+ }
9644
+ throw error;
9645
+ }
9646
+ }
9647
+ throw lastError || new Error('Maximum retry attempts exceeded');
9648
+ }
9649
+ /**
9650
+ * 단순 Chat 스트리밍 내부 구현 - /chat/stream 엔드포인트 사용
9651
+ */
9652
+ async sendChatStreamInternal(request, onChunk, onMetadata) {
9653
+ const response = await fetch(`${this.baseUrl}/chat/stream`, {
9654
+ method: 'POST',
9655
+ headers: this.getHeaders(),
9656
+ credentials: 'include',
9657
+ body: JSON.stringify(request)
9658
+ });
9659
+ if (!response.ok) {
9660
+ const error = await response.text();
9661
+ throw new Error(`Failed to send chat message: ${error}`);
9662
+ }
9663
+ const reader = response.body?.getReader();
9664
+ if (!reader) {
9665
+ throw new Error('Response body is not readable');
9666
+ }
9667
+ const decoder = new TextDecoder();
9668
+ let buffer = '';
9669
+ try {
9670
+ while (true) {
9671
+ const { done, value } = await reader.read();
9672
+ if (done)
9673
+ break;
9674
+ buffer += decoder.decode(value, { stream: true });
9675
+ const lines = buffer.split('\n');
9676
+ buffer = lines.pop() || '';
9677
+ for (const line of lines) {
9678
+ if (line.startsWith('data: ')) {
9679
+ try {
9680
+ const data = JSON.parse(line.slice(6));
9681
+ if (data.error) {
9682
+ throw new Error(data.error);
9683
+ }
9684
+ if (data.content) {
9685
+ onChunk(data.content);
9686
+ }
9687
+ if (data.done && data.conversationId && onMetadata) {
9688
+ onMetadata({ conversationId: data.conversationId });
9689
+ }
9690
+ }
9691
+ catch (e) {
9692
+ if (!(e instanceof SyntaxError)) {
9693
+ throw e;
9694
+ }
9695
+ }
9696
+ }
9697
+ }
7376
9698
  }
7377
9699
  }
7378
- throw lastError || new Error('Maximum retry attempts exceeded');
7379
- }
7380
- /**
7381
- * Execute cell action (explain, fix, custom)
7382
- * Uses global rate limit handling with key rotation
7383
- */
7384
- async cellAction(request) {
7385
- console.log('[ApiService] cellAction request:', request);
7386
- return this.fetchWithKeyRotation(`${this.baseUrl}/cell/action`, request, { defaultErrorMessage: '셀 액션 실패' });
7387
- }
7388
- /**
7389
- * Send chat message (non-streaming)
7390
- * Uses global rate limit handling with key rotation
7391
- */
7392
- async sendMessage(request) {
7393
- console.log('[ApiService] sendMessage request');
7394
- return this.fetchWithKeyRotation(`${this.baseUrl}/chat/message`, request, { defaultErrorMessage: '메시지 전송 실패' });
9700
+ finally {
9701
+ reader.releaseLock();
9702
+ }
7395
9703
  }
7396
9704
  /**
7397
- * Send chat message with streaming response
7398
- *
7399
- * NOTE (Financial Security Compliance):
7400
- * - API key rotation is handled by frontend (not server)
7401
- * - Server receives ONLY ONE key per request
7402
- * - On 429 rate limit, frontend rotates key and retries with next key
9705
+ * Agent V2용 스트리밍 - /agent/langchain/stream 사용
9706
+ * LangChain Deep Agent (HITL, Todo, 도구 실행 등)
7403
9707
  */
7404
- async sendMessageStream(request, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall) {
9708
+ async sendAgentV2Stream(request, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall, onComplete, // Callback to capture thread_id for context persistence
9709
+ threadId // Optional thread_id to continue existing conversation
9710
+ ) {
7405
9711
  // Maximum retry attempts (should match number of keys)
7406
9712
  const MAX_RETRIES = 10;
7407
9713
  let currentConfig = request.llmConfig;
@@ -7412,7 +9718,7 @@ class ApiService {
7412
9718
  ? { ...request, llmConfig: (0,_ApiKeyManager__WEBPACK_IMPORTED_MODULE_0__.buildSingleKeyConfig)(currentConfig) }
7413
9719
  : request;
7414
9720
  try {
7415
- await this.sendMessageStreamInternal(requestToSend, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall);
9721
+ await this.sendAgentV2StreamInternal(requestToSend, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall, onComplete, threadId);
7416
9722
  // Success - reset key rotation state
7417
9723
  (0,_ApiKeyManager__WEBPACK_IMPORTED_MODULE_0__.resetKeyRotation)();
7418
9724
  return;
@@ -7459,14 +9765,27 @@ class ApiService {
7459
9765
  };
7460
9766
  }
7461
9767
  /**
7462
- * Internal streaming implementation (without retry logic)
7463
- * Uses LangChain agent endpoint for improved middleware support
9768
+ * 기존 sendMessageStream 유지 (하위 호환성)
9769
+ * 내부적으로 sendAgentV2Stream 호출
9770
+ * @deprecated sendChatStream 또는 sendAgentV2Stream을 직접 사용하세요
9771
+ */
9772
+ async sendMessageStream(request, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall, onComplete, threadId) {
9773
+ return this.sendAgentV2Stream(request, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall, onComplete, threadId);
9774
+ }
9775
+ /**
9776
+ * Agent V2 스트리밍 내부 구현 - /agent/langchain/stream 사용
9777
+ * (기존 sendMessageStreamInternal에서 이름 변경)
7464
9778
  */
7465
- async sendMessageStreamInternal(request, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall) {
9779
+ async sendAgentV2StreamInternal(request, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall, onComplete, threadId) {
7466
9780
  // Convert IChatRequest to LangChain AgentRequest format
7467
9781
  // Frontend's context has limited fields, map what's available
9782
+ const resourceContext = await this.getResourceUsageSnapshot();
9783
+ const requestConfig = resourceContext && request.llmConfig
9784
+ ? { ...request.llmConfig, resourceContext }
9785
+ : request.llmConfig;
7468
9786
  const langchainRequest = {
7469
9787
  request: request.message,
9788
+ threadId: threadId,
7470
9789
  notebookContext: request.context ? {
7471
9790
  notebook_path: request.context.notebookPath,
7472
9791
  cell_count: 0,
@@ -7474,15 +9793,22 @@ class ApiService {
7474
9793
  defined_variables: [],
7475
9794
  recent_cells: request.context.selectedCells?.map(cell => ({ source: cell })) || []
7476
9795
  } : undefined,
7477
- llmConfig: request.llmConfig,
9796
+ llmConfig: requestConfig,
7478
9797
  workspaceRoot: '.'
7479
9798
  };
9799
+ // Debug: log request size
9800
+ const requestBody = JSON.stringify(langchainRequest);
9801
+ console.log('[ApiService] Sending langchain request:', {
9802
+ messageLength: request.message.length,
9803
+ bodyLength: requestBody.length,
9804
+ threadId: threadId,
9805
+ });
7480
9806
  // Use LangChain streaming endpoint
7481
9807
  const response = await fetch(`${this.baseUrl}/agent/langchain/stream`, {
7482
9808
  method: 'POST',
7483
9809
  headers: this.getHeaders(),
7484
9810
  credentials: 'include',
7485
- body: JSON.stringify(langchainRequest)
9811
+ body: requestBody
7486
9812
  });
7487
9813
  if (!response.ok) {
7488
9814
  const error = await response.text();
@@ -7529,6 +9855,15 @@ class ApiService {
7529
9855
  if (onDebugClear) {
7530
9856
  onDebugClear();
7531
9857
  }
9858
+ // Capture thread_id for context persistence across cycles
9859
+ console.log('[ApiService] Complete event received, data:', data);
9860
+ if (onComplete && data.thread_id) {
9861
+ console.log('[ApiService] Calling onComplete with threadId:', data.thread_id);
9862
+ onComplete({ threadId: data.thread_id });
9863
+ }
9864
+ else {
9865
+ console.log('[ApiService] onComplete not called - onComplete:', !!onComplete, 'thread_id:', data.thread_id);
9866
+ }
7532
9867
  return;
7533
9868
  }
7534
9869
  // Handle errors
@@ -7561,7 +9896,9 @@ class ApiService {
7561
9896
  onToolCall({
7562
9897
  tool: data.tool,
7563
9898
  code: data.code,
7564
- content: data.content
9899
+ content: data.content,
9900
+ command: data.command,
9901
+ timeout: data.timeout
7565
9902
  });
7566
9903
  currentEventType = '';
7567
9904
  continue;
@@ -7612,6 +9949,10 @@ class ApiService {
7612
9949
  * Resume interrupted agent execution with user decision
7613
9950
  */
7614
9951
  async resumeAgent(threadId, decision, args, feedback, llmConfig, onChunk, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall) {
9952
+ const resourceContext = await this.getResourceUsageSnapshot();
9953
+ const requestConfig = resourceContext && llmConfig
9954
+ ? { ...llmConfig, resourceContext }
9955
+ : llmConfig;
7615
9956
  const resumeRequest = {
7616
9957
  threadId,
7617
9958
  decisions: [{
@@ -7619,7 +9960,7 @@ class ApiService {
7619
9960
  args,
7620
9961
  feedback
7621
9962
  }],
7622
- llmConfig,
9963
+ llmConfig: requestConfig,
7623
9964
  workspaceRoot: '.'
7624
9965
  };
7625
9966
  const response = await fetch(`${this.baseUrl}/agent/langchain/resume`, {
@@ -7704,7 +10045,9 @@ class ApiService {
7704
10045
  onToolCall({
7705
10046
  tool: data.tool,
7706
10047
  code: data.code,
7707
- content: data.content
10048
+ content: data.content,
10049
+ command: data.command,
10050
+ timeout: data.timeout
7708
10051
  });
7709
10052
  currentEventType = '';
7710
10053
  continue;
@@ -7724,6 +10067,220 @@ class ApiService {
7724
10067
  reader.releaseLock();
7725
10068
  }
7726
10069
  }
10070
+ async executeCommand(command, timeout, cwd, stdin) {
10071
+ const response = await fetch(`${this.baseUrl}/execute-command`, {
10072
+ method: 'POST',
10073
+ headers: this.getHeaders(),
10074
+ credentials: 'include',
10075
+ body: JSON.stringify({ command, timeout, cwd, stdin })
10076
+ });
10077
+ const payload = await response.json().catch(() => ({}));
10078
+ if (!response.ok) {
10079
+ const errorMessage = payload.error || 'Failed to execute command';
10080
+ throw new Error(errorMessage);
10081
+ }
10082
+ return payload;
10083
+ }
10084
+ async executeCommandStream(command, options) {
10085
+ const response = await fetch(`${this.baseUrl}/execute-command/stream`, {
10086
+ method: 'POST',
10087
+ headers: this.getHeaders(),
10088
+ credentials: 'include',
10089
+ body: JSON.stringify({
10090
+ command,
10091
+ timeout: options?.timeout,
10092
+ cwd: options?.cwd,
10093
+ stdin: options?.stdin
10094
+ })
10095
+ });
10096
+ if (!response.ok) {
10097
+ const payload = await response.json().catch(() => ({}));
10098
+ const errorMessage = payload.error || 'Failed to execute command';
10099
+ throw new Error(errorMessage);
10100
+ }
10101
+ const reader = response.body?.getReader();
10102
+ if (!reader) {
10103
+ throw new Error('Response body is not readable');
10104
+ }
10105
+ const decoder = new TextDecoder();
10106
+ let buffer = '';
10107
+ let result = null;
10108
+ let streamError = null;
10109
+ try {
10110
+ while (true) {
10111
+ const { done, value } = await reader.read();
10112
+ if (done)
10113
+ break;
10114
+ buffer += decoder.decode(value, { stream: true });
10115
+ const lines = buffer.split('\n');
10116
+ buffer = lines.pop() || '';
10117
+ let currentEventType = '';
10118
+ for (const line of lines) {
10119
+ if (line.startsWith('event: ')) {
10120
+ currentEventType = line.slice(7).trim();
10121
+ continue;
10122
+ }
10123
+ if (!line.startsWith('data: ')) {
10124
+ continue;
10125
+ }
10126
+ let data;
10127
+ try {
10128
+ data = JSON.parse(line.slice(6));
10129
+ }
10130
+ catch (e) {
10131
+ continue;
10132
+ }
10133
+ if (currentEventType === 'output') {
10134
+ if (typeof data.text === 'string' && options?.onOutput) {
10135
+ options.onOutput({
10136
+ stream: data.stream === 'stderr' ? 'stderr' : 'stdout',
10137
+ text: data.text
10138
+ });
10139
+ }
10140
+ currentEventType = '';
10141
+ continue;
10142
+ }
10143
+ if (currentEventType === 'error') {
10144
+ streamError = data.error || 'Command execution failed';
10145
+ currentEventType = '';
10146
+ continue;
10147
+ }
10148
+ if (currentEventType === 'result') {
10149
+ result = data;
10150
+ return result;
10151
+ }
10152
+ currentEventType = '';
10153
+ }
10154
+ }
10155
+ }
10156
+ finally {
10157
+ reader.releaseLock();
10158
+ }
10159
+ if (streamError) {
10160
+ throw new Error(streamError);
10161
+ }
10162
+ if (!result) {
10163
+ throw new Error('No command result received');
10164
+ }
10165
+ return result;
10166
+ }
10167
+ async readFile(path, options) {
10168
+ const response = await fetch(`${this.baseUrl}/read-file`, {
10169
+ method: 'POST',
10170
+ headers: this.getHeaders(),
10171
+ credentials: 'include',
10172
+ body: JSON.stringify({
10173
+ path,
10174
+ encoding: options?.encoding || 'utf-8',
10175
+ cwd: options?.cwd
10176
+ })
10177
+ });
10178
+ const payload = await response.json().catch(() => ({}));
10179
+ if (!response.ok) {
10180
+ const errorMessage = payload.error || 'Failed to read file';
10181
+ return { success: false, error: errorMessage };
10182
+ }
10183
+ return payload;
10184
+ }
10185
+ async writeFile(path, content, options) {
10186
+ const response = await fetch(`${this.baseUrl}/write-file`, {
10187
+ method: 'POST',
10188
+ headers: this.getHeaders(),
10189
+ credentials: 'include',
10190
+ body: JSON.stringify({
10191
+ path,
10192
+ content,
10193
+ encoding: options?.encoding,
10194
+ overwrite: options?.overwrite,
10195
+ cwd: options?.cwd
10196
+ })
10197
+ });
10198
+ const payload = await response.json().catch(() => ({}));
10199
+ if (!response.ok) {
10200
+ const errorMessage = payload.error || 'Failed to write file';
10201
+ throw new Error(errorMessage);
10202
+ }
10203
+ return payload;
10204
+ }
10205
+ /**
10206
+ * Search workspace for files matching pattern
10207
+ * Executed on Jupyter server using grep/ripgrep
10208
+ */
10209
+ async searchWorkspace(options) {
10210
+ const response = await fetch(`${this.baseUrl}/search-workspace`, {
10211
+ method: 'POST',
10212
+ headers: this.getHeaders(),
10213
+ credentials: 'include',
10214
+ body: JSON.stringify({
10215
+ pattern: options.pattern,
10216
+ file_types: options.file_types || ['*.py', '*.ipynb'],
10217
+ path: options.path || '.',
10218
+ max_results: options.max_results || 50,
10219
+ case_sensitive: options.case_sensitive || false
10220
+ })
10221
+ });
10222
+ const payload = await response.json().catch(() => ({}));
10223
+ if (!response.ok) {
10224
+ const errorMessage = payload.error || 'Failed to search workspace';
10225
+ throw new Error(errorMessage);
10226
+ }
10227
+ return payload;
10228
+ }
10229
+ /**
10230
+ * Search notebook cells for pattern
10231
+ * Executed on Jupyter server
10232
+ */
10233
+ async searchNotebookCells(options) {
10234
+ const response = await fetch(`${this.baseUrl}/search-notebook-cells`, {
10235
+ method: 'POST',
10236
+ headers: this.getHeaders(),
10237
+ credentials: 'include',
10238
+ body: JSON.stringify({
10239
+ pattern: options.pattern,
10240
+ notebook_path: options.notebook_path,
10241
+ cell_type: options.cell_type,
10242
+ max_results: options.max_results || 30,
10243
+ case_sensitive: options.case_sensitive || false
10244
+ })
10245
+ });
10246
+ const payload = await response.json().catch(() => ({}));
10247
+ if (!response.ok) {
10248
+ const errorMessage = payload.error || 'Failed to search notebook cells';
10249
+ throw new Error(errorMessage);
10250
+ }
10251
+ return payload;
10252
+ }
10253
+ /**
10254
+ * Check system resources and file sizes before data processing
10255
+ * Executed on Jupyter server
10256
+ */
10257
+ async checkResource(options) {
10258
+ const response = await fetch(`${this.baseUrl}/check-resource`, {
10259
+ method: 'POST',
10260
+ headers: this.getHeaders(),
10261
+ credentials: 'include',
10262
+ body: JSON.stringify({
10263
+ files: options.files || [],
10264
+ dataframes: options.dataframes || [],
10265
+ file_size_command: options.file_size_command || '',
10266
+ dataframe_check_code: options.dataframe_check_code || ''
10267
+ })
10268
+ });
10269
+ const payload = await response.json().catch(() => ({}));
10270
+ if (!response.ok) {
10271
+ if (response.status === 404) {
10272
+ return {
10273
+ success: false,
10274
+ files: [],
10275
+ dataframes: [],
10276
+ error: 'Resource check endpoint is not available on this Jupyter server.'
10277
+ };
10278
+ }
10279
+ const errorMessage = payload.error || 'Failed to check resources';
10280
+ throw new Error(errorMessage);
10281
+ }
10282
+ return payload;
10283
+ }
7727
10284
  /**
7728
10285
  * Save configuration
7729
10286
  */
@@ -9238,9 +11795,9 @@ class ToolExecutor {
9238
11795
  },
9239
11796
  });
9240
11797
  }
9241
- // execute_command 도구 등록 (조건부 승인)
9242
- const executeCommandDef = _ToolRegistry__WEBPACK_IMPORTED_MODULE_1__.BUILTIN_TOOL_DEFINITIONS.find(t => t.name === 'execute_command');
9243
- if (executeCommandDef && !this.registry.hasTool('execute_command')) {
11798
+ // execute_command_tool 도구 등록 (조건부 승인)
11799
+ const executeCommandDef = _ToolRegistry__WEBPACK_IMPORTED_MODULE_1__.BUILTIN_TOOL_DEFINITIONS.find(t => t.name === 'execute_command_tool');
11800
+ if (executeCommandDef && !this.registry.hasTool('execute_command_tool')) {
9244
11801
  this.registry.register({
9245
11802
  ...executeCommandDef,
9246
11803
  executor: async (params, context) => {
@@ -9620,6 +12177,11 @@ class ToolExecutor {
9620
12177
  isDangerousCommand(command) {
9621
12178
  return _ToolRegistry__WEBPACK_IMPORTED_MODULE_1__.DANGEROUS_COMMAND_PATTERNS.some(pattern => pattern.test(command));
9622
12179
  }
12180
+ summarizeOutput(output, maxLines = 2) {
12181
+ const lines = output.split(/\r?\n/).filter(line => line.length > 0);
12182
+ const text = lines.slice(0, maxLines).join('\n');
12183
+ return { text, truncated: lines.length > maxLines };
12184
+ }
9623
12185
  /**
9624
12186
  * read_file 도구: 파일 읽기
9625
12187
  */
@@ -10037,19 +12599,19 @@ print(json.dumps(result))
10037
12599
  }
10038
12600
  }
10039
12601
  /**
10040
- * execute_command 도구: 셸 명령 실행 (조건부 승인)
12602
+ * execute_command_tool 도구: 셸 명령 실행 (조건부 승인)
10041
12603
  */
10042
12604
  async executeCommand(params, context) {
10043
12605
  console.log('[ToolExecutor] executeCommand:', params.command);
10044
- const timeout = params.timeout || 30000;
12606
+ const timeout = typeof params.timeout === 'number' ? params.timeout : 600000;
10045
12607
  // 위험 명령 검사 및 조건부 승인 요청
10046
12608
  if (this.isDangerousCommand(params.command)) {
10047
12609
  console.log('[ToolExecutor] Dangerous command detected, requesting approval');
10048
12610
  // 승인 요청
10049
12611
  const request = {
10050
- id: `execute_command-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
10051
- toolName: 'execute_command',
10052
- toolDefinition: this.registry.getTool('execute_command'),
12612
+ id: `execute_command_tool-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
12613
+ toolName: 'execute_command_tool',
12614
+ toolDefinition: this.registry.getTool('execute_command_tool'),
10053
12615
  parameters: params,
10054
12616
  stepNumber: context.stepNumber,
10055
12617
  description: `🔴 위험 명령 실행 요청:\n\n\`${params.command}\`\n\n이 명령은 시스템에 영향을 줄 수 있습니다.`,
@@ -10066,56 +12628,32 @@ print(json.dumps(result))
10066
12628
  }
10067
12629
  }
10068
12630
  }
10069
- // Python subprocess로 명령 실행
10070
- const pythonCode = `
10071
- import json
10072
- import subprocess
10073
- import sys
10074
- try:
10075
- command = ${JSON.stringify(params.command)}
10076
- timeout_sec = ${timeout / 1000}
10077
-
10078
- result = subprocess.run(
10079
- command,
10080
- shell=True,
10081
- capture_output=True,
10082
- text=True,
10083
- timeout=timeout_sec
10084
- )
10085
-
10086
- output = {
10087
- 'success': result.returncode == 0,
10088
- 'stdout': result.stdout,
10089
- 'stderr': result.stderr,
10090
- 'returncode': result.returncode
10091
- }
10092
- except subprocess.TimeoutExpired:
10093
- output = {'success': False, 'error': f'Command timed out after {timeout_sec}s'}
10094
- except Exception as e:
10095
- output = {'success': False, 'error': str(e)}
10096
- print(json.dumps(output))
10097
- `.trim();
12631
+ if (!this.apiService) {
12632
+ return { success: false, error: 'ApiService not available for execute_command_tool' };
12633
+ }
10098
12634
  try {
10099
- const execResult = await this.executeInKernel(pythonCode);
10100
- if (execResult.status === 'ok' && execResult.stdout) {
10101
- const parsed = JSON.parse(execResult.stdout.trim());
10102
- if (parsed.success) {
10103
- return {
10104
- success: true,
10105
- output: parsed.stdout || '(no output)',
10106
- };
10107
- }
10108
- else {
10109
- return {
10110
- success: false,
10111
- error: parsed.error || parsed.stderr || `Command failed with code ${parsed.returncode}`,
10112
- };
10113
- }
12635
+ const result = await this.apiService.executeCommandStream(params.command, { timeout });
12636
+ const stdout = typeof result.stdout === 'string' ? result.stdout : '';
12637
+ const stderr = typeof result.stderr === 'string' ? result.stderr : '';
12638
+ const combined = [stdout, stderr].filter(Boolean).join('\n');
12639
+ const summary = this.summarizeOutput(combined, 2);
12640
+ const output = summary.text || '(no output)';
12641
+ if (result.success) {
12642
+ return {
12643
+ success: true,
12644
+ output,
12645
+ };
10114
12646
  }
10115
- return { success: false, error: execResult.error?.evalue || 'Command execution failed' };
12647
+ const errorText = summary.text || result.error || stderr || `Command failed with code ${result.returncode}`;
12648
+ return {
12649
+ success: false,
12650
+ error: errorText,
12651
+ };
10116
12652
  }
10117
12653
  catch (error) {
10118
- return { success: false, error: error.message };
12654
+ const message = error instanceof Error ? error.message : 'Command execution failed';
12655
+ const summary = this.summarizeOutput(String(message), 2);
12656
+ return { success: false, error: summary.text || 'Command execution failed' };
10119
12657
  }
10120
12658
  }
10121
12659
  /**
@@ -11901,7 +14439,7 @@ const BUILTIN_TOOL_DEFINITIONS = [
11901
14439
  // 확장 도구 (시스템)
11902
14440
  // ─────────────────────────────────────────────────────────────────────────
11903
14441
  {
11904
- name: 'execute_command',
14442
+ name: 'execute_command_tool',
11905
14443
  description: '셸 명령 실행 (위험 명령만 승인 필요)',
11906
14444
  riskLevel: 'critical',
11907
14445
  requiresApproval: false,
@@ -11986,7 +14524,7 @@ const BUILTIN_TOOL_DEFINITIONS = [
11986
14524
  ];
11987
14525
  /**
11988
14526
  * 위험한 셸 명령 패턴들
11989
- * execute_command에서 이 패턴에 매칭되는 명령은 사용자 승인 필요
14527
+ * execute_command_tool에서 이 패턴에 매칭되는 명령은 사용자 승인 필요
11990
14528
  */
11991
14529
  const DANGEROUS_COMMAND_PATTERNS = [
11992
14530
  // 파일 삭제/제거
@@ -12452,9 +14990,11 @@ class SafetyChecker {
12452
14990
 
12453
14991
  __webpack_require__.r(__webpack_exports__);
12454
14992
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
14993
+ /* harmony export */ countDiffLines: () => (/* binding */ countDiffLines),
12455
14994
  /* harmony export */ escapeHtml: () => (/* binding */ escapeHtml),
12456
14995
  /* harmony export */ formatInlineMarkdown: () => (/* binding */ formatInlineMarkdown),
12457
14996
  /* harmony export */ formatMarkdownToHtml: () => (/* binding */ formatMarkdownToHtml),
14997
+ /* harmony export */ highlightDiff: () => (/* binding */ highlightDiff),
12458
14998
  /* harmony export */ highlightJavaScript: () => (/* binding */ highlightJavaScript),
12459
14999
  /* harmony export */ highlightPython: () => (/* binding */ highlightPython),
12460
15000
  /* harmony export */ normalizeIndentation: () => (/* binding */ normalizeIndentation),
@@ -12515,6 +15055,154 @@ function normalizeIndentation(code) {
12515
15055
  });
12516
15056
  return normalized.join('\n');
12517
15057
  }
15058
+ function extractNextItemsBlock(text) {
15059
+ if (!text.trim())
15060
+ return null;
15061
+ const fencedRegex = /```(\w+)?\n([\s\S]*?)```/g;
15062
+ let match;
15063
+ while ((match = fencedRegex.exec(text)) !== null) {
15064
+ const lang = (match[1] || '').toLowerCase();
15065
+ const content = match[2].trim();
15066
+ if (!content)
15067
+ continue;
15068
+ if (lang && lang !== 'json' && !content.includes('"next_items"')) {
15069
+ continue;
15070
+ }
15071
+ const items = parseNextItemsPayload(content);
15072
+ if (items) {
15073
+ const placeholder = `__NEXT_ITEMS_${Math.random().toString(36).slice(2, 11)}__`;
15074
+ const updated = text.slice(0, match.index) + placeholder + text.slice(match.index + match[0].length);
15075
+ return { items, placeholder, text: updated };
15076
+ }
15077
+ }
15078
+ const range = findNextItemsJsonRange(text);
15079
+ if (!range)
15080
+ return null;
15081
+ const candidate = text.slice(range.start, range.end + 1);
15082
+ const items = parseNextItemsPayload(candidate);
15083
+ if (!items)
15084
+ return null;
15085
+ let replacementStart = range.start;
15086
+ const lineStart = text.lastIndexOf('\n', range.start - 1) + 1;
15087
+ const linePrefix = text.slice(lineStart, range.start).trim();
15088
+ const normalizedPrefix = linePrefix.replace(/[::]$/, '').toLowerCase();
15089
+ if (normalizedPrefix === 'json') {
15090
+ replacementStart = lineStart;
15091
+ }
15092
+ const placeholder = `__NEXT_ITEMS_${Math.random().toString(36).slice(2, 11)}__`;
15093
+ const updated = text.slice(0, replacementStart) + placeholder + text.slice(range.end + 1);
15094
+ return { items, placeholder, text: updated };
15095
+ }
15096
+ function findNextItemsJsonRange(text) {
15097
+ const key = '"next_items"';
15098
+ let searchIndex = text.indexOf(key);
15099
+ while (searchIndex !== -1) {
15100
+ const start = text.lastIndexOf('{', searchIndex);
15101
+ if (start === -1)
15102
+ return null;
15103
+ const end = findMatchingBrace(text, start);
15104
+ if (end !== -1) {
15105
+ return { start, end };
15106
+ }
15107
+ searchIndex = text.indexOf(key, searchIndex + key.length);
15108
+ }
15109
+ return null;
15110
+ }
15111
+ function findMatchingBrace(text, start) {
15112
+ let depth = 0;
15113
+ let inString = false;
15114
+ let escaped = false;
15115
+ for (let i = start; i < text.length; i++) {
15116
+ const char = text[i];
15117
+ if (inString) {
15118
+ if (escaped) {
15119
+ escaped = false;
15120
+ continue;
15121
+ }
15122
+ if (char === '\\') {
15123
+ escaped = true;
15124
+ continue;
15125
+ }
15126
+ if (char === '"') {
15127
+ inString = false;
15128
+ }
15129
+ continue;
15130
+ }
15131
+ if (char === '"') {
15132
+ inString = true;
15133
+ continue;
15134
+ }
15135
+ if (char === '{') {
15136
+ depth += 1;
15137
+ }
15138
+ else if (char === '}') {
15139
+ depth -= 1;
15140
+ if (depth === 0) {
15141
+ return i;
15142
+ }
15143
+ }
15144
+ }
15145
+ return -1;
15146
+ }
15147
+ function parseNextItemsPayload(payload) {
15148
+ if (!payload.startsWith('{') || !payload.endsWith('}'))
15149
+ return null;
15150
+ try {
15151
+ const parsed = JSON.parse(payload);
15152
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
15153
+ return null;
15154
+ const nextItemsRaw = parsed.next_items;
15155
+ if (!Array.isArray(nextItemsRaw))
15156
+ return null;
15157
+ const items = nextItemsRaw
15158
+ .map((item) => {
15159
+ if (!item || typeof item !== 'object')
15160
+ return null;
15161
+ const subject = typeof item.subject === 'string'
15162
+ ? item.subject.trim()
15163
+ : '';
15164
+ const description = typeof item.description === 'string'
15165
+ ? item.description.trim()
15166
+ : '';
15167
+ if (!subject && !description)
15168
+ return null;
15169
+ return { subject, description };
15170
+ })
15171
+ .filter((item) => Boolean(item));
15172
+ return items.length > 0 ? items : null;
15173
+ }
15174
+ catch {
15175
+ return null;
15176
+ }
15177
+ }
15178
+ function renderNextItemsList(items) {
15179
+ // Simple arrow icon
15180
+ const arrowSvg = `
15181
+ <svg class="jp-next-items-icon" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
15182
+ <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"/>
15183
+ </svg>`;
15184
+ const listItems = items.map((item) => {
15185
+ const subject = escapeHtml(item.subject);
15186
+ const description = escapeHtml(item.description);
15187
+ const subjectHtml = subject ? `<div class="jp-next-items-subject">${subject}</div>` : '';
15188
+ const descriptionHtml = description ? `<div class="jp-next-items-description">${description}</div>` : '';
15189
+ return `
15190
+ <li class="jp-next-items-item" data-next-item="true" role="button" tabindex="0">
15191
+ <div class="jp-next-items-text">
15192
+ ${subjectHtml}
15193
+ ${descriptionHtml}
15194
+ </div>
15195
+ ${arrowSvg}
15196
+ </li>`;
15197
+ }).join('');
15198
+ return `
15199
+ <div class="jp-next-items" data-next-items="true">
15200
+ <div class="jp-next-items-header">다음 단계 제안</div>
15201
+ <ul class="jp-next-items-list" role="list">
15202
+ ${listItems}
15203
+ </ul>
15204
+ </div>`;
15205
+ }
12518
15206
  /**
12519
15207
  * Highlight Python code with inline styles
12520
15208
  */
@@ -12640,6 +15328,52 @@ function highlightPython(code) {
12640
15328
  });
12641
15329
  return highlighted;
12642
15330
  }
15331
+ /**
15332
+ * Highlight diff output with colors for additions/deletions
15333
+ */
15334
+ function highlightDiff(code) {
15335
+ const lines = code.split('\n');
15336
+ const highlightedLines = lines.map(line => {
15337
+ const escapedLine = escapeHtml(line);
15338
+ // File headers (--- and +++)
15339
+ if (line.startsWith('---') || line.startsWith('+++')) {
15340
+ return `<span class="diff-header">${escapedLine}</span>`;
15341
+ }
15342
+ // Hunk headers (@@ ... @@)
15343
+ if (line.startsWith('@@')) {
15344
+ return `<span class="diff-hunk">${escapedLine}</span>`;
15345
+ }
15346
+ // Added lines
15347
+ if (line.startsWith('+')) {
15348
+ return `<span class="diff-add">${escapedLine}</span>`;
15349
+ }
15350
+ // Removed lines
15351
+ if (line.startsWith('-')) {
15352
+ return `<span class="diff-del">${escapedLine}</span>`;
15353
+ }
15354
+ // Context lines (unchanged)
15355
+ return `<span class="diff-context">${escapedLine}</span>`;
15356
+ });
15357
+ return highlightedLines.join('\n');
15358
+ }
15359
+ /**
15360
+ * Count additions and deletions in diff code
15361
+ */
15362
+ function countDiffLines(code) {
15363
+ const lines = code.split('\n');
15364
+ let additions = 0;
15365
+ let deletions = 0;
15366
+ for (const line of lines) {
15367
+ // Skip file headers
15368
+ if (line.startsWith('+++') || line.startsWith('---'))
15369
+ continue;
15370
+ if (line.startsWith('+'))
15371
+ additions++;
15372
+ else if (line.startsWith('-'))
15373
+ deletions++;
15374
+ }
15375
+ return { additions, deletions };
15376
+ }
12643
15377
  /**
12644
15378
  * Highlight JavaScript code
12645
15379
  */
@@ -12915,9 +15649,10 @@ function parseMarkdownTable(tableText) {
12915
15649
  * Format markdown text to HTML with syntax highlighting
12916
15650
  */
12917
15651
  function formatMarkdownToHtml(text) {
15652
+ const nextItemsBlock = extractNextItemsBlock(text);
12918
15653
  // Decode HTML entities if present
12919
15654
  const textarea = document.createElement('textarea');
12920
- textarea.innerHTML = text;
15655
+ textarea.innerHTML = nextItemsBlock ? nextItemsBlock.text : text;
12921
15656
  let html = textarea.value;
12922
15657
  // Step 0.5: Protect DataFrame HTML tables (must be before code blocks)
12923
15658
  const dataframeHtmlPlaceholders = [];
@@ -12945,20 +15680,36 @@ function formatMarkdownToHtml(text) {
12945
15680
  language: lang
12946
15681
  });
12947
15682
  // Create HTML for code block
12948
- const highlightedCode = lang === 'python' || lang === 'py'
12949
- ? highlightPython(trimmedCode)
12950
- : lang === 'javascript' || lang === 'js'
12951
- ? highlightJavaScript(trimmedCode)
12952
- : escapeHtml(trimmedCode);
12953
- const htmlBlock = '<div class="code-block-container" data-block-id="' + blockId + '">' +
15683
+ const isDiff = lang === 'diff';
15684
+ const highlightedCode = isDiff
15685
+ ? highlightDiff(trimmedCode)
15686
+ : lang === 'python' || lang === 'py'
15687
+ ? highlightPython(trimmedCode)
15688
+ : lang === 'javascript' || lang === 'js'
15689
+ ? highlightJavaScript(trimmedCode)
15690
+ : escapeHtml(trimmedCode);
15691
+ // For diff blocks, calculate and show line counts
15692
+ let diffLineCountHtml = '';
15693
+ if (isDiff) {
15694
+ const { additions, deletions } = countDiffLines(trimmedCode);
15695
+ diffLineCountHtml = '<span class="diff-line-counts">' +
15696
+ '<span class="diff-additions">+' + additions + '</span>' +
15697
+ '<span class="diff-deletions">-' + deletions + '</span>' +
15698
+ '</span>';
15699
+ }
15700
+ const htmlBlock = '<div class="code-block-container' + (isDiff ? ' diff-block' : '') + '" data-block-id="' + blockId + '">' +
12954
15701
  '<div class="code-block-header">' +
12955
15702
  '<span class="code-block-language">' + escapeHtml(lang) + '</span>' +
15703
+ diffLineCountHtml +
12956
15704
  '<div class="code-block-actions">' +
12957
- '<button class="code-block-apply" data-block-id="' + blockId + '" title="셀에 적용">셀에 적용</button>' +
15705
+ (isDiff ? '' : '<button class="code-block-apply" data-block-id="' + blockId + '" title="셀에 적용">셀에 적용</button>') +
12958
15706
  '<button class="code-block-copy" data-block-id="' + blockId + '" title="복사">복사</button>' +
12959
15707
  '</div>' +
12960
15708
  '</div>' +
12961
15709
  '<pre class="code-block language-' + escapeHtml(lang) + '"><code id="' + blockId + '">' + highlightedCode + '</code></pre>' +
15710
+ '<button class="code-block-toggle" data-block-id="' + blockId + '" title="전체 보기" aria-label="전체 보기" aria-expanded="false">' +
15711
+ '<span class="code-block-toggle-icon" aria-hidden="true">▾</span>' +
15712
+ '</button>' +
12962
15713
  '</div>';
12963
15714
  codeBlockPlaceholders.push({
12964
15715
  placeholder: placeholder,
@@ -13020,7 +15771,7 @@ function formatMarkdownToHtml(text) {
13020
15771
  return '\n' + placeholder + '\n';
13021
15772
  });
13022
15773
  // Step 4: Escape HTML for non-placeholder text
13023
- html = html.split(/(__(?:DATAFRAME_HTML|CODE_BLOCK|INLINE_CODE|TABLE)_[a-z0-9-]+__)/gi)
15774
+ html = html.split(/(__(?:DATAFRAME_HTML|CODE_BLOCK|INLINE_CODE|TABLE|NEXT_ITEMS)_[a-z0-9-]+__)/gi)
13024
15775
  .map((part, index) => {
13025
15776
  // Odd indices are placeholders - keep as is
13026
15777
  if (index % 2 === 1)
@@ -13068,6 +15819,10 @@ function formatMarkdownToHtml(text) {
13068
15819
  codeBlockPlaceholders.forEach(item => {
13069
15820
  html = html.replace(item.placeholder, item.html);
13070
15821
  });
15822
+ // Step 8.5: Restore next items list placeholders
15823
+ if (nextItemsBlock) {
15824
+ html = html.split(nextItemsBlock.placeholder).join(renderNextItemsList(nextItemsBlock.items));
15825
+ }
13071
15826
  return html;
13072
15827
  }
13073
15828
 
@@ -13229,4 +15984,4 @@ __webpack_require__.r(__webpack_exports__);
13229
15984
  /***/ }
13230
15985
 
13231
15986
  }]);
13232
- //# sourceMappingURL=lib_index_js.a223ea20056954479ae9.js.map
15987
+ //# sourceMappingURL=lib_index_js.e4ff4b5779b5e049f84c.js.map