hdsp-jupyter-extension 2.0.6__py3-none-any.whl → 2.0.7__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 (90) hide show
  1. agent_server/core/reflection_engine.py +0 -1
  2. agent_server/knowledge/watchdog_service.py +1 -1
  3. agent_server/langchain/ARCHITECTURE.md +1193 -0
  4. agent_server/langchain/agent.py +74 -588
  5. agent_server/langchain/custom_middleware.py +636 -0
  6. agent_server/langchain/executors/__init__.py +2 -7
  7. agent_server/langchain/executors/notebook_searcher.py +46 -38
  8. agent_server/langchain/hitl_config.py +66 -0
  9. agent_server/langchain/llm_factory.py +166 -0
  10. agent_server/langchain/logging_utils.py +184 -0
  11. agent_server/langchain/prompts.py +119 -0
  12. agent_server/langchain/state.py +16 -6
  13. agent_server/langchain/tools/__init__.py +6 -0
  14. agent_server/langchain/tools/file_tools.py +91 -129
  15. agent_server/langchain/tools/jupyter_tools.py +18 -18
  16. agent_server/langchain/tools/resource_tools.py +161 -0
  17. agent_server/langchain/tools/search_tools.py +198 -216
  18. agent_server/langchain/tools/shell_tools.py +54 -0
  19. agent_server/main.py +4 -1
  20. agent_server/routers/health.py +1 -1
  21. agent_server/routers/langchain_agent.py +940 -285
  22. hdsp_agent_core/prompts/auto_agent_prompts.py +3 -3
  23. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.7.data}/data/share/jupyter/labextensions/hdsp-agent/build_log.json +1 -1
  24. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.7.data}/data/share/jupyter/labextensions/hdsp-agent/package.json +2 -2
  25. 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.7.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.4770ec0fb2d173b6deb4.js +312 -6
  26. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.4770ec0fb2d173b6deb4.js.map +1 -0
  27. hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.a223ea20056954479ae9.js → hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.29cf4312af19e86f82af.js +1547 -330
  28. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.29cf4312af19e86f82af.js.map +1 -0
  29. hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.addf2fa038fa60304aa2.js → hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.61343eb4cf0577e74b50.js +8 -8
  30. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.61343eb4cf0577e74b50.js.map +1 -0
  31. hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js → hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js +209 -2
  32. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js.map +1 -0
  33. jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js → hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js +2 -209
  34. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js.map +1 -0
  35. hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js → hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js +3 -212
  36. hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js.map +1 -0
  37. {hdsp_jupyter_extension-2.0.6.dist-info → hdsp_jupyter_extension-2.0.7.dist-info}/METADATA +2 -1
  38. {hdsp_jupyter_extension-2.0.6.dist-info → hdsp_jupyter_extension-2.0.7.dist-info}/RECORD +71 -68
  39. jupyter_ext/_version.py +1 -1
  40. jupyter_ext/handlers.py +1176 -58
  41. jupyter_ext/labextension/build_log.json +1 -1
  42. jupyter_ext/labextension/package.json +2 -2
  43. jupyter_ext/labextension/static/{frontend_styles_index_js.02d346171474a0fb2dc1.js → frontend_styles_index_js.4770ec0fb2d173b6deb4.js} +312 -6
  44. jupyter_ext/labextension/static/frontend_styles_index_js.4770ec0fb2d173b6deb4.js.map +1 -0
  45. jupyter_ext/labextension/static/{lib_index_js.a223ea20056954479ae9.js → lib_index_js.29cf4312af19e86f82af.js} +1547 -330
  46. jupyter_ext/labextension/static/lib_index_js.29cf4312af19e86f82af.js.map +1 -0
  47. jupyter_ext/labextension/static/{remoteEntry.addf2fa038fa60304aa2.js → remoteEntry.61343eb4cf0577e74b50.js} +8 -8
  48. jupyter_ext/labextension/static/remoteEntry.61343eb4cf0577e74b50.js.map +1 -0
  49. jupyter_ext/labextension/static/{vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js → vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js} +209 -2
  50. jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js.map +1 -0
  51. hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js → jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js +2 -209
  52. jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js.map +1 -0
  53. jupyter_ext/labextension/static/{vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js → vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js} +3 -212
  54. jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js.map +1 -0
  55. jupyter_ext/resource_usage.py +180 -0
  56. jupyter_ext/tests/test_handlers.py +58 -0
  57. agent_server/langchain/executors/jupyter_executor.py +0 -429
  58. agent_server/langchain/middleware/__init__.py +0 -36
  59. agent_server/langchain/middleware/code_search_middleware.py +0 -278
  60. agent_server/langchain/middleware/error_handling_middleware.py +0 -338
  61. agent_server/langchain/middleware/jupyter_execution_middleware.py +0 -301
  62. agent_server/langchain/middleware/rag_middleware.py +0 -227
  63. agent_server/langchain/middleware/validation_middleware.py +0 -240
  64. hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.02d346171474a0fb2dc1.js.map +0 -1
  65. hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.a223ea20056954479ae9.js.map +0 -1
  66. hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.addf2fa038fa60304aa2.js.map +0 -1
  67. hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js.map +0 -1
  68. hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js.map +0 -1
  69. hdsp_jupyter_extension-2.0.6.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js.map +0 -1
  70. jupyter_ext/labextension/static/frontend_styles_index_js.02d346171474a0fb2dc1.js.map +0 -1
  71. jupyter_ext/labextension/static/lib_index_js.a223ea20056954479ae9.js.map +0 -1
  72. jupyter_ext/labextension/static/remoteEntry.addf2fa038fa60304aa2.js.map +0 -1
  73. jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js.map +0 -1
  74. jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js.map +0 -1
  75. jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js.map +0 -1
  76. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.7.data}/data/etc/jupyter/jupyter_server_config.d/hdsp_jupyter_extension.json +0 -0
  77. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.7.data}/data/share/jupyter/labextensions/hdsp-agent/install.json +0 -0
  78. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.7.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
  79. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.7.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
  80. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.7.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
  81. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.7.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
  82. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.7.data}/data/share/jupyter/labextensions/hdsp-agent/static/style.js +0 -0
  83. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.7.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
  84. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.7.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
  85. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.7.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_styled_dist_emotion-styled_browser_development_esm_js.661fb5836f4978a7c6e1.js +0 -0
  86. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.7.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
  87. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.7.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js +0 -0
  88. {hdsp_jupyter_extension-2.0.6.data → hdsp_jupyter_extension-2.0.7.data}/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_index_js.985697e0162d8d088ca2.js.map +0 -0
  89. {hdsp_jupyter_extension-2.0.6.dist-info → hdsp_jupyter_extension-2.0.7.dist-info}/WHEEL +0 -0
  90. {hdsp_jupyter_extension-2.0.6.dist-info → hdsp_jupyter_extension-2.0.7.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,203 @@ 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 agentThreadId:', agentThreadId);
1092
+ await apiService.sendMessageStream({
1093
+ message: messageToSend,
1094
+ conversationId: conversationId || undefined,
1095
+ llmConfig: currentConfig // Include API keys with request
1096
+ },
1097
+ // onChunk callback - update message content incrementally
1098
+ (chunk) => {
1099
+ streamedContent += chunk;
1100
+ setMessages(prev => prev.map(msg => msg.id === assistantMessageId && isChatMessage(msg)
1101
+ ? { ...msg, content: streamedContent }
1102
+ : msg));
1103
+ },
1104
+ // onMetadata callback - update conversationId and metadata
1105
+ (metadata) => {
1106
+ if (metadata.conversationId && !conversationId) {
1107
+ setConversationId(metadata.conversationId);
1108
+ }
1109
+ if (metadata.provider || metadata.model) {
1110
+ setMessages(prev => prev.map(msg => msg.id === assistantMessageId && isChatMessage(msg)
1111
+ ? {
1112
+ ...msg,
1113
+ metadata: {
1114
+ ...msg.metadata,
1115
+ provider: metadata.provider,
1116
+ model: metadata.model
1117
+ }
1118
+ }
1119
+ : msg));
1120
+ }
1121
+ },
1122
+ // onDebug callback - show debug status in gray
1123
+ (status) => {
1124
+ setDebugStatus(status);
1125
+ },
1126
+ // onInterrupt callback - show approval dialog
1127
+ (interrupt) => {
1128
+ approvalPendingRef.current = true;
1129
+ // Capture threadId from interrupt for context persistence
1130
+ if (interrupt.threadId && !agentThreadId) {
1131
+ setAgentThreadId(interrupt.threadId);
1132
+ console.log('[AgentPanel] Captured agentThreadId from interrupt:', interrupt.threadId);
1133
+ }
1134
+ // Auto-approve search/file/resource tools - execute immediately without user interaction
1135
+ if (interrupt.action === 'search_workspace_tool'
1136
+ || interrupt.action === 'search_notebook_cells_tool'
1137
+ || interrupt.action === 'check_resource_tool'
1138
+ || interrupt.action === 'list_files_tool'
1139
+ || interrupt.action === 'read_file_tool') {
1140
+ void handleAutoToolInterrupt(interrupt);
1141
+ return;
1142
+ }
1143
+ if (autoApproveEnabled) {
1144
+ void resumeFromInterrupt(interrupt, 'approve');
1145
+ return;
1146
+ }
1147
+ if (interrupt.action === 'jupyter_cell_tool' && interrupt.args?.code) {
1148
+ const shouldQueue = shouldExecuteInNotebook(interrupt.args.code);
1149
+ if (isAutoApprovedCode(interrupt.args.code)) {
1150
+ if (shouldQueue) {
1151
+ queueApprovalCell(interrupt.args.code);
1152
+ }
1153
+ void resumeFromInterrupt(interrupt, 'approve');
1154
+ return;
1155
+ }
1156
+ if (shouldQueue) {
1157
+ queueApprovalCell(interrupt.args.code);
1158
+ }
1159
+ }
1160
+ setInterruptData(interrupt);
1161
+ upsertInterruptMessage(interrupt);
1162
+ setIsLoading(false);
1163
+ setIsStreaming(false);
1164
+ },
1165
+ // onTodos callback - update todo list UI
1166
+ (newTodos) => {
1167
+ setTodos(newTodos);
1168
+ },
1169
+ // onDebugClear callback - clear debug status
1170
+ () => {
1171
+ setDebugStatus(null);
1172
+ },
1173
+ // onToolCall callback - add cells to notebook
1174
+ handleToolCall,
1175
+ // onComplete callback - capture thread_id for context persistence
1176
+ (data) => {
1177
+ if (data.threadId) {
1178
+ setAgentThreadId(data.threadId);
1179
+ console.log('[AgentPanel] Captured agentThreadId for context persistence:', data.threadId);
1180
+ }
1181
+ },
1182
+ // threadId - pass existing thread_id to continue context
1183
+ agentThreadId || undefined);
1184
+ }
1185
+ catch (error) {
1186
+ const message = error instanceof Error ? error.message : 'Failed to send message';
1187
+ setDebugStatus(`오류: ${message}`);
1188
+ // Update the assistant message with error
1189
+ setMessages(prev => prev.map(msg => msg.id === assistantMessageId && isChatMessage(msg)
1190
+ ? {
1191
+ ...msg,
1192
+ content: streamedContent + `\n\nError: ${message}`
1193
+ }
1194
+ : msg));
1195
+ }
1196
+ finally {
1197
+ setIsLoading(false);
1198
+ setIsStreaming(false);
1199
+ setStreamingMessageId(null);
1200
+ // Keep completed todos visible after the run
1201
+ }
1202
+ };
975
1203
  // Extract and store code blocks from messages, setup button listeners
976
1204
  (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
977
1205
  // Use a small delay to ensure DOM is updated after message rendering
@@ -1014,6 +1242,40 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
1014
1242
  // Use event delegation - attach single listener to container
1015
1243
  const handleContainerClick = async (e) => {
1016
1244
  const target = e.target;
1245
+ const nextItem = target.closest('.jp-next-items-item');
1246
+ if (nextItem) {
1247
+ e.stopPropagation();
1248
+ e.preventDefault();
1249
+ const subject = nextItem.querySelector('.jp-next-items-subject')?.textContent?.trim() || '';
1250
+ const description = nextItem.querySelector('.jp-next-items-description')?.textContent?.trim() || '';
1251
+ const nextText = subject && description
1252
+ ? `${subject}\n\n${description}`
1253
+ : (subject || description);
1254
+ if (nextText) {
1255
+ void handleNextItemSelection(nextText);
1256
+ }
1257
+ return;
1258
+ }
1259
+ // Handle expand/collapse button
1260
+ if (target.classList.contains('code-block-toggle') || target.closest('.code-block-toggle')) {
1261
+ const button = target.classList.contains('code-block-toggle')
1262
+ ? target
1263
+ : target.closest('.code-block-toggle');
1264
+ e.stopPropagation();
1265
+ e.preventDefault();
1266
+ const container = button.closest('.code-block-container');
1267
+ if (!container)
1268
+ return;
1269
+ const isExpanded = container.classList.toggle('is-expanded');
1270
+ button.setAttribute('aria-expanded', String(isExpanded));
1271
+ button.setAttribute('title', isExpanded ? '접기' : '전체 보기');
1272
+ button.setAttribute('aria-label', isExpanded ? '접기' : '전체 보기');
1273
+ const icon = button.querySelector('.code-block-toggle-icon');
1274
+ if (icon) {
1275
+ icon.textContent = isExpanded ? '▴' : '▾';
1276
+ }
1277
+ return;
1278
+ }
1017
1279
  // Handle copy button
1018
1280
  if (target.classList.contains('code-block-copy') || target.closest('.code-block-copy')) {
1019
1281
  const button = target.classList.contains('code-block-copy')
@@ -1570,6 +1832,12 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
1570
1832
  if (lines.length === 0) {
1571
1833
  return true;
1572
1834
  }
1835
+ if (lines.some(line => (line.startsWith('!')
1836
+ || line.startsWith('%%bash')
1837
+ || line.startsWith('%%sh')
1838
+ || line.startsWith('%%shell')))) {
1839
+ return false;
1840
+ }
1573
1841
  const disallowedPatterns = [
1574
1842
  /(^|[^=!<>])=([^=]|$)/,
1575
1843
  /\.read_[a-zA-Z0-9_]*\s*\(/,
@@ -1591,6 +1859,160 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
1591
1859
  ];
1592
1860
  return lines.every(line => allowedPatterns.some(pattern => pattern.test(line)));
1593
1861
  };
1862
+ const userRequestedNotebookExecution = () => {
1863
+ const lastUserMessage = [...messages]
1864
+ .reverse()
1865
+ .find((msg) => isChatMessage(msg) && msg.role === 'user');
1866
+ const content = lastUserMessage?.content ?? '';
1867
+ return /노트북|셀|cell|notebook|jupyter/i.test(content);
1868
+ };
1869
+ const isShellCell = (code) => {
1870
+ const lines = code.split('\n');
1871
+ const firstLine = lines.find(line => line.trim().length > 0)?.trim() || '';
1872
+ if (firstLine.startsWith('%%bash')
1873
+ || firstLine.startsWith('%%sh')
1874
+ || firstLine.startsWith('%%shell')) {
1875
+ return true;
1876
+ }
1877
+ return lines.some(line => line.trim().startsWith('!'));
1878
+ };
1879
+ const extractShellCommand = (code) => {
1880
+ const lines = code.split('\n');
1881
+ const firstNonEmptyIndex = lines.findIndex(line => line.trim().length > 0);
1882
+ if (firstNonEmptyIndex === -1) {
1883
+ return '';
1884
+ }
1885
+ const firstLine = lines[firstNonEmptyIndex].trim();
1886
+ if (firstLine.startsWith('%%bash')
1887
+ || firstLine.startsWith('%%sh')
1888
+ || firstLine.startsWith('%%shell')) {
1889
+ const script = lines.slice(firstNonEmptyIndex + 1).join('\n').trim();
1890
+ if (!script) {
1891
+ return '';
1892
+ }
1893
+ const escaped = script
1894
+ .replace(/\\/g, '\\\\')
1895
+ .replace(/'/g, "\\'")
1896
+ .replace(/\r?\n/g, '\\n');
1897
+ return `bash -lc $'${escaped}'`;
1898
+ }
1899
+ const shellLines = lines
1900
+ .map(line => line.trim())
1901
+ .filter(line => line.startsWith('!'))
1902
+ .map(line => line.replace(/^!+/, '').trim())
1903
+ .filter(Boolean);
1904
+ return shellLines.join('\n');
1905
+ };
1906
+ const buildPythonCommand = (code) => (`python3 -c ${JSON.stringify(code)}`);
1907
+ const shouldExecuteInNotebook = (code) => {
1908
+ const notebook = getActiveNotebookPanel();
1909
+ if (!notebook) {
1910
+ return false;
1911
+ }
1912
+ if (isShellCell(code) && !userRequestedNotebookExecution()) {
1913
+ return false;
1914
+ }
1915
+ return true;
1916
+ };
1917
+ const truncateOutputLines = (output, maxLines = 2) => {
1918
+ const lines = output.split(/\r?\n/).filter(line => line.length > 0);
1919
+ const text = lines.slice(0, maxLines).join('\n');
1920
+ return { text, truncated: lines.length > maxLines };
1921
+ };
1922
+ const createCommandOutputMessage = (command) => {
1923
+ const messageId = makeMessageId('command-output');
1924
+ const outputMessage = {
1925
+ id: messageId,
1926
+ role: 'system',
1927
+ content: `🐚 ${command}\n`,
1928
+ timestamp: Date.now(),
1929
+ metadata: {
1930
+ kind: 'shell-output',
1931
+ command
1932
+ }
1933
+ };
1934
+ setMessages(prev => [...prev, outputMessage]);
1935
+ return messageId;
1936
+ };
1937
+ const appendCommandOutputMessage = (messageId, text, stream) => {
1938
+ if (!text)
1939
+ return;
1940
+ const prefix = stream === 'stderr' ? '[stderr] ' : '';
1941
+ setMessages(prev => prev.map(msg => {
1942
+ if (msg.id !== messageId || !('role' in msg)) {
1943
+ return msg;
1944
+ }
1945
+ const chatMsg = msg;
1946
+ return {
1947
+ ...chatMsg,
1948
+ content: `${chatMsg.content}${prefix}${text}`
1949
+ };
1950
+ }));
1951
+ };
1952
+ const executeSubprocessCommand = async (command, timeout, onOutput, stdin) => {
1953
+ try {
1954
+ const result = await apiService.executeCommandStream(command, { timeout, onOutput, stdin });
1955
+ const stdout = typeof result.stdout === 'string' ? result.stdout : '';
1956
+ const stderr = typeof result.stderr === 'string' ? result.stderr : '';
1957
+ const combined = [stdout, stderr].filter(Boolean).join('\n');
1958
+ const summary = truncateOutputLines(combined, 2);
1959
+ const truncated = summary.truncated || Boolean(result.truncated);
1960
+ const output = summary.text || '(no output)';
1961
+ if (result.success) {
1962
+ return {
1963
+ success: true,
1964
+ output,
1965
+ returncode: result.returncode ?? null,
1966
+ command,
1967
+ truncated
1968
+ };
1969
+ }
1970
+ const errorText = summary.text || result.error || stderr || 'Command failed';
1971
+ return {
1972
+ success: false,
1973
+ output,
1974
+ error: errorText,
1975
+ returncode: result.returncode ?? null,
1976
+ command,
1977
+ truncated
1978
+ };
1979
+ }
1980
+ catch (error) {
1981
+ const message = error instanceof Error ? error.message : 'Command execution failed';
1982
+ if (onOutput) {
1983
+ onOutput({ stream: 'stderr', text: `${message}\n` });
1984
+ }
1985
+ const summary = truncateOutputLines(message, 2);
1986
+ return {
1987
+ success: false,
1988
+ output: '',
1989
+ error: summary.text || 'Command execution failed',
1990
+ returncode: null,
1991
+ command,
1992
+ truncated: summary.truncated
1993
+ };
1994
+ }
1995
+ };
1996
+ const executeCodeViaSubprocess = async (code, timeout) => {
1997
+ const isShell = isShellCell(code);
1998
+ const command = isShell ? extractShellCommand(code) : buildPythonCommand(code);
1999
+ if (!command) {
2000
+ return {
2001
+ success: false,
2002
+ output: '',
2003
+ error: isShell ? 'Shell command is empty' : 'Python command is empty',
2004
+ returncode: null,
2005
+ command,
2006
+ truncated: false,
2007
+ execution_method: 'subprocess'
2008
+ };
2009
+ }
2010
+ const result = await executeSubprocessCommand(command, timeout);
2011
+ return {
2012
+ ...result,
2013
+ execution_method: 'subprocess'
2014
+ };
2015
+ };
1594
2016
  const upsertInterruptMessage = (interrupt) => {
1595
2017
  const interruptMessageId = interruptMessageIdRef.current || makeMessageId('interrupt');
1596
2018
  interruptMessageIdRef.current = interruptMessageId;
@@ -1609,7 +2031,7 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
1609
2031
  return [...prev, interruptMessage];
1610
2032
  });
1611
2033
  };
1612
- const clearInterruptMessage = () => {
2034
+ const clearInterruptMessage = (decision) => {
1613
2035
  if (!interruptMessageIdRef.current)
1614
2036
  return;
1615
2037
  const messageId = interruptMessageIdRef.current;
@@ -1624,7 +2046,8 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
1624
2046
  ...chatMsg.metadata,
1625
2047
  interrupt: {
1626
2048
  ...(chatMsg.metadata?.interrupt || {}),
1627
- resolved: true
2049
+ resolved: true,
2050
+ decision: decision || 'approve' // Track approve/reject decision
1628
2051
  }
1629
2052
  }
1630
2053
  };
@@ -1632,38 +2055,454 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
1632
2055
  return msg;
1633
2056
  }));
1634
2057
  };
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
- };
1650
- }
1651
- else if (executed && executed.tool === 'markdown') {
1652
- resumeDecision = 'edit';
1653
- resumeArgs = {
1654
- content: executed.content
1655
- };
1656
- }
2058
+ const validateRelativePath = (path) => {
2059
+ const trimmed = path.trim();
2060
+ if (trimmed.startsWith('/') || trimmed.startsWith('\\') || /^[A-Za-z]:/.test(trimmed)) {
2061
+ return { valid: false, error: 'Absolute paths are not allowed' };
1657
2062
  }
1658
- else {
1659
- pendingToolCallsRef.current.shift();
1660
- approvalPendingRef.current = pendingToolCallsRef.current.length > 0;
2063
+ if (trimmed.includes('..')) {
2064
+ return { valid: false, error: 'Path traversal (..) is not allowed' };
1661
2065
  }
1662
- setIsLoading(true);
1663
- setIsStreaming(true);
1664
- let interrupted = false;
1665
- // 항상 메시지 생성 - 승인 UI 아래에 append되도록
1666
- const assistantMessageId = makeMessageId('assistant');
2066
+ return { valid: true };
2067
+ };
2068
+ const normalizeContentsPath = (path) => {
2069
+ const trimmed = path.trim();
2070
+ if (!trimmed || trimmed === '.' || trimmed === './') {
2071
+ return '';
2072
+ }
2073
+ return trimmed
2074
+ .replace(/^\.\/+/, '')
2075
+ .replace(/^\/+/, '')
2076
+ .replace(/\/+$/, '');
2077
+ };
2078
+ const globToRegex = (pattern) => {
2079
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, '\\$&');
2080
+ const regex = `^${escaped.replace(/\*/g, '.*').replace(/\?/g, '.')}$`;
2081
+ return new RegExp(regex);
2082
+ };
2083
+ const fetchContentsModel = async (path, options) => {
2084
+ try {
2085
+ const { PageConfig, URLExt } = await Promise.resolve(/*! import() */).then(__webpack_require__.t.bind(__webpack_require__, /*! @jupyterlab/coreutils */ "webpack/sharing/consume/default/@jupyterlab/coreutils", 23));
2086
+ const baseUrl = PageConfig.getBaseUrl();
2087
+ const normalizedPath = normalizeContentsPath(path);
2088
+ const apiUrl = URLExt.join(baseUrl, 'api/contents', normalizedPath);
2089
+ const query = new URLSearchParams();
2090
+ if (options?.content !== undefined) {
2091
+ query.set('content', options.content ? '1' : '0');
2092
+ }
2093
+ if (options?.format) {
2094
+ query.set('format', options.format);
2095
+ }
2096
+ const url = query.toString() ? `${apiUrl}?${query.toString()}` : apiUrl;
2097
+ const response = await fetch(url, {
2098
+ method: 'GET',
2099
+ credentials: 'include',
2100
+ });
2101
+ if (!response.ok) {
2102
+ return { success: false, error: `Failed to load contents: ${response.status}` };
2103
+ }
2104
+ const data = await response.json();
2105
+ return { success: true, data };
2106
+ }
2107
+ catch (error) {
2108
+ const message = error instanceof Error ? error.message : 'Failed to load contents';
2109
+ return { success: false, error: message };
2110
+ }
2111
+ };
2112
+ const executeListFilesTool = async (params) => {
2113
+ const pathCheck = validateRelativePath(params.path);
2114
+ if (!pathCheck.valid) {
2115
+ return { success: false, error: pathCheck.error };
2116
+ }
2117
+ const pattern = (params.pattern || '*').trim() || '*';
2118
+ const recursive = params.recursive ?? false;
2119
+ const matcher = globToRegex(pattern);
2120
+ const maxEntries = 500;
2121
+ const files = [];
2122
+ const pendingDirs = [normalizeContentsPath(params.path)];
2123
+ const visited = new Set();
2124
+ while (pendingDirs.length > 0 && files.length < maxEntries) {
2125
+ const dirPath = pendingDirs.shift() ?? '';
2126
+ if (visited.has(dirPath)) {
2127
+ continue;
2128
+ }
2129
+ visited.add(dirPath);
2130
+ const contentsResult = await fetchContentsModel(dirPath, { content: true });
2131
+ if (!contentsResult.success) {
2132
+ return { success: false, error: contentsResult.error };
2133
+ }
2134
+ const model = contentsResult.data;
2135
+ if (!model || model.type !== 'directory' || !Array.isArray(model.content)) {
2136
+ const displayPath = dirPath || '.';
2137
+ return { success: false, error: `Not a directory: ${displayPath}` };
2138
+ }
2139
+ for (const entry of model.content) {
2140
+ if (!entry) {
2141
+ continue;
2142
+ }
2143
+ const name = entry.name || entry.path?.split('/').pop() || '';
2144
+ const entryPath = entry.path || name;
2145
+ const isDir = entry.type === 'directory';
2146
+ if (matcher.test(name)) {
2147
+ files.push({
2148
+ path: entryPath,
2149
+ isDir,
2150
+ size: isDir ? 0 : (entry.size ?? 0),
2151
+ });
2152
+ }
2153
+ if (recursive && isDir && entryPath) {
2154
+ pendingDirs.push(entryPath);
2155
+ }
2156
+ if (files.length >= maxEntries) {
2157
+ break;
2158
+ }
2159
+ }
2160
+ }
2161
+ const formatted = files.map((file) => {
2162
+ const icon = file.isDir ? '📁' : '📄';
2163
+ const sizeInfo = file.isDir ? '' : ` (${file.size} bytes)`;
2164
+ return `${icon} ${file.path}${file.isDir ? '/' : sizeInfo}`;
2165
+ }).join('\n');
2166
+ return {
2167
+ success: true,
2168
+ output: formatted || '(empty directory)',
2169
+ metadata: { count: files.length, files }
2170
+ };
2171
+ };
2172
+ const executeReadFileTool = async (params) => {
2173
+ if (!params.path) {
2174
+ return { success: false, error: 'Path is required' };
2175
+ }
2176
+ const pathCheck = validateRelativePath(params.path);
2177
+ if (!pathCheck.valid) {
2178
+ return { success: false, error: pathCheck.error };
2179
+ }
2180
+ const maxLines = typeof params.maxLines === 'number' ? params.maxLines : 1000;
2181
+ const safeMaxLines = Math.max(0, maxLines);
2182
+ const contentsResult = await fetchContentsModel(params.path, { content: true, format: 'text' });
2183
+ if (!contentsResult.success) {
2184
+ return { success: false, error: contentsResult.error };
2185
+ }
2186
+ const model = contentsResult.data;
2187
+ if (!model) {
2188
+ return { success: false, error: 'File not found' };
2189
+ }
2190
+ if (model.type === 'directory') {
2191
+ return { success: false, error: `Path is a directory: ${params.path}` };
2192
+ }
2193
+ let content = model.content ?? '';
2194
+ if (model.format === 'base64') {
2195
+ return { success: false, error: 'Binary file content is not supported' };
2196
+ }
2197
+ if (typeof content !== 'string') {
2198
+ content = JSON.stringify(content, null, 2);
2199
+ }
2200
+ const lines = content.split('\n');
2201
+ const sliced = lines.slice(0, safeMaxLines);
2202
+ return {
2203
+ success: true,
2204
+ output: sliced.join('\n'),
2205
+ metadata: {
2206
+ lineCount: sliced.length,
2207
+ truncated: lines.length > safeMaxLines
2208
+ }
2209
+ };
2210
+ };
2211
+ // Helper function for auto-approving search/file tools with execution results
2212
+ const handleAutoToolInterrupt = async (interrupt) => {
2213
+ const { threadId, action, args } = interrupt;
2214
+ console.log('[AgentPanel] Auto-approving tool:', action, args);
2215
+ try {
2216
+ let executionResult;
2217
+ if (action === 'search_workspace_tool') {
2218
+ setDebugStatus(`🔍 검색 실행 중: ${args?.pattern || ''}`);
2219
+ executionResult = await apiService.searchWorkspace({
2220
+ pattern: args?.pattern || '',
2221
+ file_types: args?.file_types || ['*.py', '*.ipynb'],
2222
+ path: args?.path || '.',
2223
+ max_results: args?.max_results || 50,
2224
+ case_sensitive: args?.case_sensitive || false
2225
+ });
2226
+ console.log('[AgentPanel] search_workspace result:', executionResult);
2227
+ }
2228
+ else if (action === 'search_notebook_cells_tool') {
2229
+ setDebugStatus(`🔍 노트북 검색 실행 중: ${args?.pattern || ''}`);
2230
+ executionResult = await apiService.searchNotebookCells({
2231
+ pattern: args?.pattern || '',
2232
+ notebook_path: args?.notebook_path,
2233
+ cell_type: args?.cell_type,
2234
+ max_results: args?.max_results || 30,
2235
+ case_sensitive: args?.case_sensitive || false
2236
+ });
2237
+ console.log('[AgentPanel] search_notebook_cells result:', executionResult);
2238
+ }
2239
+ else if (action === 'check_resource_tool') {
2240
+ const filesList = Array.isArray(args?.files) ? args.files : (args?.files ? [args.files] : []);
2241
+ setDebugStatus(`📊 리소스 체크 중: ${filesList.join(', ') || 'system'}`);
2242
+ executionResult = await apiService.checkResource({
2243
+ files: filesList,
2244
+ dataframes: args?.dataframes || [],
2245
+ file_size_command: args?.file_size_command || '',
2246
+ dataframe_check_code: args?.dataframe_check_code || ''
2247
+ });
2248
+ console.log('[AgentPanel] check_resource result:', executionResult);
2249
+ }
2250
+ else if (action === 'list_files_tool') {
2251
+ setDebugStatus('📂 파일 목록 조회 중...');
2252
+ const listParams = {
2253
+ path: typeof args?.path === 'string' ? args.path : '.',
2254
+ recursive: args?.recursive ?? false,
2255
+ pattern: args?.pattern ?? undefined
2256
+ };
2257
+ executionResult = await executeListFilesTool(listParams);
2258
+ console.log('[AgentPanel] list_files result:', executionResult);
2259
+ }
2260
+ else if (action === 'read_file_tool') {
2261
+ setDebugStatus('📄 파일 읽는 중...');
2262
+ const readParams = {
2263
+ path: typeof args?.path === 'string' ? args.path : '',
2264
+ encoding: typeof args?.encoding === 'string' ? args.encoding : undefined,
2265
+ maxLines: args?.max_lines ?? args?.maxLines
2266
+ };
2267
+ executionResult = await executeReadFileTool(readParams);
2268
+ console.log('[AgentPanel] read_file result:', executionResult);
2269
+ }
2270
+ else {
2271
+ console.warn('[AgentPanel] Unknown auto tool:', action);
2272
+ return;
2273
+ }
2274
+ // Resume with execution result
2275
+ const resumeArgs = {
2276
+ ...args,
2277
+ execution_result: executionResult
2278
+ };
2279
+ // Clear interrupt state (don't show approval UI for search)
2280
+ setInterruptData(null);
2281
+ // Create new assistant message for continued response
2282
+ const assistantMessageId = makeMessageId('assistant');
2283
+ setStreamingMessageId(assistantMessageId);
2284
+ setMessages(prev => [
2285
+ ...prev,
2286
+ {
2287
+ id: assistantMessageId,
2288
+ role: 'assistant',
2289
+ content: '',
2290
+ timestamp: Date.now()
2291
+ }
2292
+ ]);
2293
+ let streamedContent = '';
2294
+ let interrupted = false;
2295
+ setDebugStatus('🤔 LLM 응답 대기 중');
2296
+ await apiService.resumeAgent(threadId, 'edit', // Use 'edit' to pass execution_result in args
2297
+ resumeArgs, undefined, llmConfig || undefined, (chunk) => {
2298
+ streamedContent += chunk;
2299
+ setMessages(prev => prev.map(msg => msg.id === assistantMessageId && isChatMessage(msg)
2300
+ ? { ...msg, content: streamedContent }
2301
+ : msg));
2302
+ }, (status) => {
2303
+ setDebugStatus(status);
2304
+ }, (nextInterrupt) => {
2305
+ interrupted = true;
2306
+ approvalPendingRef.current = true;
2307
+ const autoApproveEnabled = getAutoApproveEnabled(llmConfig || (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_4__.getLLMConfig)() || (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_4__.getDefaultLLMConfig)());
2308
+ // Handle next interrupt (could be another search or code execution)
2309
+ if (nextInterrupt.action === 'search_workspace_tool'
2310
+ || nextInterrupt.action === 'search_notebook_cells_tool'
2311
+ || nextInterrupt.action === 'check_resource_tool'
2312
+ || nextInterrupt.action === 'list_files_tool'
2313
+ || nextInterrupt.action === 'read_file_tool') {
2314
+ void handleAutoToolInterrupt(nextInterrupt);
2315
+ return;
2316
+ }
2317
+ if (autoApproveEnabled) {
2318
+ void resumeFromInterrupt(nextInterrupt, 'approve');
2319
+ return;
2320
+ }
2321
+ if (nextInterrupt.action === 'jupyter_cell_tool' && nextInterrupt.args?.code) {
2322
+ const shouldQueue = shouldExecuteInNotebook(nextInterrupt.args.code);
2323
+ if (isAutoApprovedCode(nextInterrupt.args.code)) {
2324
+ if (shouldQueue) {
2325
+ queueApprovalCell(nextInterrupt.args.code);
2326
+ }
2327
+ void resumeFromInterrupt(nextInterrupt, 'approve');
2328
+ return;
2329
+ }
2330
+ if (shouldQueue) {
2331
+ queueApprovalCell(nextInterrupt.args.code);
2332
+ }
2333
+ }
2334
+ setInterruptData(nextInterrupt);
2335
+ upsertInterruptMessage(nextInterrupt);
2336
+ setIsLoading(false);
2337
+ setIsStreaming(false);
2338
+ }, (newTodos) => {
2339
+ setTodos(newTodos);
2340
+ }, () => {
2341
+ setDebugStatus(null);
2342
+ }, handleToolCall);
2343
+ if (!interrupted) {
2344
+ setIsLoading(false);
2345
+ setIsStreaming(false);
2346
+ setStreamingMessageId(null);
2347
+ approvalPendingRef.current = false;
2348
+ }
2349
+ }
2350
+ catch (error) {
2351
+ const message = error instanceof Error ? error.message : 'Tool execution failed';
2352
+ console.error('[AgentPanel] Auto tool error:', error);
2353
+ setDebugStatus(`오류: ${message}`);
2354
+ setIsLoading(false);
2355
+ setIsStreaming(false);
2356
+ approvalPendingRef.current = false;
2357
+ }
2358
+ };
2359
+ const resumeFromInterrupt = async (interrupt, decision, feedback // Optional feedback message for rejection
2360
+ ) => {
2361
+ const { threadId } = interrupt;
2362
+ setInterruptData(null);
2363
+ setDebugStatus(null);
2364
+ clearInterruptMessage(decision); // Pass decision to track approve/reject
2365
+ let resumeDecision = decision;
2366
+ let resumeArgs = undefined;
2367
+ if (decision === 'approve') {
2368
+ // Handle write_file_tool separately - execute file write on Jupyter server
2369
+ if (interrupt.action === 'write_file_tool') {
2370
+ try {
2371
+ setDebugStatus('📝 파일 쓰기 중...');
2372
+ const writeResult = await apiService.writeFile(interrupt.args?.path || '', interrupt.args?.content || '', {
2373
+ encoding: interrupt.args?.encoding || 'utf-8',
2374
+ overwrite: interrupt.args?.overwrite || false
2375
+ });
2376
+ console.log('[AgentPanel] write_file result:', writeResult);
2377
+ resumeDecision = 'edit';
2378
+ resumeArgs = {
2379
+ ...interrupt.args,
2380
+ execution_result: writeResult
2381
+ };
2382
+ }
2383
+ catch (error) {
2384
+ const message = error instanceof Error ? error.message : 'File write failed';
2385
+ console.error('[AgentPanel] write_file error:', error);
2386
+ resumeDecision = 'edit';
2387
+ resumeArgs = {
2388
+ ...interrupt.args,
2389
+ execution_result: { success: false, error: message }
2390
+ };
2391
+ }
2392
+ }
2393
+ else if (interrupt.action === 'execute_command_tool') {
2394
+ const command = (interrupt.args?.command || '').trim();
2395
+ // Default stdin to "y\n" for interactive prompts (yes/no)
2396
+ const stdinInput = interrupt.args?.stdin ?? 'y\n';
2397
+ setDebugStatus('🐚 셸 명령 실행 중...');
2398
+ const outputMessageId = command ? createCommandOutputMessage(command) : null;
2399
+ const execResult = command
2400
+ ? await executeSubprocessCommand(command, interrupt.args?.timeout, outputMessageId
2401
+ ? (chunk) => appendCommandOutputMessage(outputMessageId, chunk.text, chunk.stream)
2402
+ : undefined, stdinInput)
2403
+ : {
2404
+ success: false,
2405
+ output: '',
2406
+ error: 'Command is required',
2407
+ returncode: null,
2408
+ command,
2409
+ truncated: false
2410
+ };
2411
+ resumeDecision = 'edit';
2412
+ resumeArgs = {
2413
+ ...interrupt.args,
2414
+ execution_result: execResult
2415
+ };
2416
+ }
2417
+ else if (interrupt.action === 'jupyter_cell_tool' && interrupt.args?.code) {
2418
+ const code = interrupt.args.code;
2419
+ if (!shouldExecuteInNotebook(code)) {
2420
+ setDebugStatus('🐚 서브프로세스로 코드 실행 중...');
2421
+ const execResult = await executeCodeViaSubprocess(code, interrupt.args?.timeout);
2422
+ resumeDecision = 'edit';
2423
+ resumeArgs = {
2424
+ code,
2425
+ execution_result: execResult
2426
+ };
2427
+ }
2428
+ else {
2429
+ const executed = await executePendingApproval();
2430
+ if (executed && executed.tool === 'jupyter_cell') {
2431
+ resumeDecision = 'edit';
2432
+ resumeArgs = {
2433
+ code: executed.code,
2434
+ execution_result: executed.execution_result
2435
+ };
2436
+ }
2437
+ else {
2438
+ const notebook = getActiveNotebookPanel();
2439
+ if (!notebook) {
2440
+ setDebugStatus('🐚 노트북이 없어 서브프로세스로 실행 중...');
2441
+ const execResult = await executeCodeViaSubprocess(code, interrupt.args?.timeout);
2442
+ resumeDecision = 'edit';
2443
+ resumeArgs = {
2444
+ code,
2445
+ execution_result: execResult
2446
+ };
2447
+ }
2448
+ else {
2449
+ const cellIndex = insertCell(notebook, 'code', code);
2450
+ if (cellIndex === null) {
2451
+ setDebugStatus('🐚 노트북 모델이 없어 서브프로세스로 실행 중...');
2452
+ const execResult = await executeCodeViaSubprocess(code, interrupt.args?.timeout);
2453
+ resumeDecision = 'edit';
2454
+ resumeArgs = {
2455
+ code,
2456
+ execution_result: execResult
2457
+ };
2458
+ }
2459
+ else {
2460
+ await executeCell(notebook, cellIndex);
2461
+ const execResult = captureExecutionResult(notebook, cellIndex);
2462
+ resumeDecision = 'edit';
2463
+ resumeArgs = {
2464
+ code,
2465
+ execution_result: execResult
2466
+ };
2467
+ }
2468
+ }
2469
+ }
2470
+ }
2471
+ }
2472
+ else {
2473
+ const executed = await executePendingApproval();
2474
+ if (executed && executed.tool === 'jupyter_cell') {
2475
+ resumeDecision = 'edit';
2476
+ resumeArgs = {
2477
+ code: executed.code,
2478
+ execution_result: executed.execution_result
2479
+ };
2480
+ }
2481
+ else if (executed && executed.tool === 'markdown') {
2482
+ resumeDecision = 'edit';
2483
+ resumeArgs = {
2484
+ content: executed.content
2485
+ };
2486
+ }
2487
+ }
2488
+ }
2489
+ else {
2490
+ // Reject: delete the pending cell from notebook
2491
+ const pendingCall = pendingToolCallsRef.current[0];
2492
+ if (pendingCall && pendingCall.cellIndex !== undefined) {
2493
+ const notebook = getActiveNotebookPanel();
2494
+ if (notebook) {
2495
+ deleteCell(notebook, pendingCall.cellIndex);
2496
+ }
2497
+ }
2498
+ pendingToolCallsRef.current.shift();
2499
+ approvalPendingRef.current = pendingToolCallsRef.current.length > 0;
2500
+ }
2501
+ setIsLoading(true);
2502
+ setIsStreaming(true);
2503
+ let interrupted = false;
2504
+ // 항상 새 메시지 생성 - 승인 UI 아래에 append되도록
2505
+ const assistantMessageId = makeMessageId('assistant');
1667
2506
  setStreamingMessageId(assistantMessageId);
1668
2507
  // 새 메시지 추가 (맨 아래에 append)
1669
2508
  setMessages(prev => [
@@ -1676,8 +2515,12 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
1676
2515
  }
1677
2516
  ]);
1678
2517
  let streamedContent = '';
2518
+ // Build feedback message for rejection
2519
+ const rejectionFeedback = resumeDecision === 'reject'
2520
+ ? (feedback ? `사용자 피드백: ${feedback}` : 'User rejected this action')
2521
+ : undefined;
1679
2522
  try {
1680
- await apiService.resumeAgent(threadId, resumeDecision, resumeArgs, resumeDecision === 'reject' ? 'User rejected this action' : undefined, llmConfig || undefined, (chunk) => {
2523
+ await apiService.resumeAgent(threadId, resumeDecision, resumeArgs, rejectionFeedback, llmConfig || undefined, (chunk) => {
1681
2524
  streamedContent += chunk;
1682
2525
  setMessages(prev => prev.map(msg => msg.id === assistantMessageId && isChatMessage(msg)
1683
2526
  ? { ...msg, content: streamedContent }
@@ -1687,13 +2530,32 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
1687
2530
  }, (nextInterrupt) => {
1688
2531
  interrupted = true;
1689
2532
  approvalPendingRef.current = true;
2533
+ const autoApproveEnabled = getAutoApproveEnabled(llmConfig || (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_4__.getLLMConfig)() || (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_4__.getDefaultLLMConfig)());
2534
+ // Auto-approve search/file/resource tools
2535
+ if (nextInterrupt.action === 'search_workspace_tool'
2536
+ || nextInterrupt.action === 'search_notebook_cells_tool'
2537
+ || nextInterrupt.action === 'check_resource_tool'
2538
+ || nextInterrupt.action === 'list_files_tool'
2539
+ || nextInterrupt.action === 'read_file_tool') {
2540
+ void handleAutoToolInterrupt(nextInterrupt);
2541
+ return;
2542
+ }
2543
+ if (autoApproveEnabled) {
2544
+ void resumeFromInterrupt(nextInterrupt, 'approve');
2545
+ return;
2546
+ }
1690
2547
  if (nextInterrupt.action === 'jupyter_cell_tool' && nextInterrupt.args?.code) {
2548
+ const shouldQueue = shouldExecuteInNotebook(nextInterrupt.args.code);
1691
2549
  if (isAutoApprovedCode(nextInterrupt.args.code)) {
1692
- queueApprovalCell(nextInterrupt.args.code);
2550
+ if (shouldQueue) {
2551
+ queueApprovalCell(nextInterrupt.args.code);
2552
+ }
1693
2553
  void resumeFromInterrupt(nextInterrupt, 'approve');
1694
2554
  return;
1695
2555
  }
1696
- queueApprovalCell(nextInterrupt.args.code);
2556
+ if (shouldQueue) {
2557
+ queueApprovalCell(nextInterrupt.args.code);
2558
+ }
1697
2559
  }
1698
2560
  setInterruptData(nextInterrupt);
1699
2561
  upsertInterruptMessage(nextInterrupt);
@@ -1728,6 +2590,26 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
1728
2590
  }
1729
2591
  };
1730
2592
  const handleSendMessage = async () => {
2593
+ // Handle rejection mode - resume with optional feedback
2594
+ if (isRejectionMode && pendingRejectionInterrupt) {
2595
+ const feedback = input.trim() || undefined; // Empty input means no feedback
2596
+ // Add user message bubble if there's feedback
2597
+ if (feedback) {
2598
+ const userMessage = {
2599
+ id: makeMessageId(),
2600
+ role: 'user',
2601
+ content: feedback,
2602
+ timestamp: Date.now(),
2603
+ };
2604
+ setMessages(prev => [...prev, userMessage]);
2605
+ }
2606
+ setInput('');
2607
+ setIsRejectionMode(false);
2608
+ const interruptToResume = pendingRejectionInterrupt;
2609
+ setPendingRejectionInterrupt(null);
2610
+ await resumeFromInterrupt(interruptToResume, 'reject', feedback);
2611
+ return;
2612
+ }
1731
2613
  // Check if there's an LLM prompt stored (from cell action)
1732
2614
  const textarea = messagesEndRef.current?.parentElement?.querySelector('.jp-agent-input');
1733
2615
  const llmPrompt = pendingLlmPromptRef.current || textarea?.getAttribute('data-llm-prompt');
@@ -1753,19 +2635,23 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
1753
2635
  if (!notebook) {
1754
2636
  // Python 에러가 감지되면 파일 수정 모드로 전환
1755
2637
  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;
2638
+ console.log('[AgentPanel] Agent mode: No notebook, but Python error detected - attempting file fix');
2639
+ // Python 에러 수정 처리 시도
2640
+ const handled = await handlePythonErrorFix(currentInput);
2641
+ if (handled) {
2642
+ // 파일 수정 모드로 처리됨
2643
+ const userMessage = {
2644
+ id: makeMessageId(),
2645
+ role: 'user',
2646
+ content: currentInput,
2647
+ timestamp: Date.now(),
2648
+ };
2649
+ setMessages(prev => [...prev, userMessage]);
2650
+ setInput('');
2651
+ return;
2652
+ }
2653
+ // 파일 경로를 찾지 못함 - 아래 로직으로 계속 진행
2654
+ console.log('[AgentPanel] Agent mode: No file path found, continuing...');
1769
2655
  }
1770
2656
  // 파일 수정 관련 자연어 요청 감지 (에러, 고쳐, 수정, fix 등)
1771
2657
  const fileFixRequestPatterns = [
@@ -1849,174 +2735,66 @@ SyntaxError: '(' was never closed
1849
2735
  const userMessage = {
1850
2736
  id: makeMessageId(),
1851
2737
  role: 'user',
1852
- content: currentInput,
1853
- timestamp: Date.now(),
1854
- };
1855
- setMessages(prev => [...prev, userMessage]);
1856
- setInput('');
1857
- // Agent 실행
1858
- await handleAgentExecution(agentRequest);
1859
- return;
1860
- }
1861
- }
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
- }
1900
- // Use the display prompt (input) for the user message, or use a fallback if input is empty
1901
- 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));
2738
+ content: currentInput,
2739
+ timestamp: Date.now(),
2740
+ };
2741
+ setMessages(prev => [...prev, userMessage]);
2742
+ setInput('');
2743
+ // Agent 실행
2744
+ await handleAgentExecution(agentRequest);
2745
+ return;
2746
+ }
2008
2747
  }
2009
- finally {
2010
- setIsLoading(false);
2011
- setIsStreaming(false);
2012
- setStreamingMessageId(null);
2013
- // Keep completed todos visible after the run
2748
+ // Python 에러 감지 및 파일 수정 모드 (Chat 모드에서만)
2749
+ // 파일 경로를 추출할 수 있는 경우에만 파일 수정 모드 사용
2750
+ // 그렇지 않으면 일반 LLM 스트림으로 처리
2751
+ if (inputMode === 'chat' && detectPythonError(currentInput)) {
2752
+ console.log('[AgentPanel] Python error detected in message, attempting file fix...');
2753
+ // Python 에러 수정 처리 시도 (파일 경로를 찾을 수 있는 경우에만 처리됨)
2754
+ const handled = await handlePythonErrorFix(currentInput);
2755
+ if (handled) {
2756
+ // 파일 수정 모드로 처리됨 - 사용자 메시지 추가 후 종료
2757
+ const userMessage = {
2758
+ id: makeMessageId(),
2759
+ role: 'user',
2760
+ content: currentInput,
2761
+ timestamp: Date.now(),
2762
+ };
2763
+ setMessages(prev => [...prev, userMessage]);
2764
+ setInput('');
2765
+ return;
2766
+ }
2767
+ // 파일 경로를 찾지 못해 handled=false 반환됨
2768
+ // 일반 스트림으로 폴백 (아래 로직에서 사용자 메시지 추가됨)
2769
+ console.log('[AgentPanel] No file path found in error, using regular LLM stream');
2014
2770
  }
2771
+ // Use the display prompt (input) for the user message, or use a fallback if input is empty
2772
+ const displayContent = currentInput || (llmPrompt ? '셀 분석 요청' : '');
2773
+ await sendChatMessage({
2774
+ displayContent,
2775
+ llmPrompt,
2776
+ textarea,
2777
+ clearInput: Boolean(currentInput)
2778
+ });
2015
2779
  };
2016
2780
  // Handle resume after user approval/rejection
2017
2781
  const handleResumeAgent = async (decision) => {
2018
2782
  if (!interruptData)
2019
2783
  return;
2784
+ if (decision === 'reject') {
2785
+ // Enter rejection mode - wait for user feedback before resuming
2786
+ setIsRejectionMode(true);
2787
+ setPendingRejectionInterrupt(interruptData);
2788
+ // Clear the interrupt UI but keep the data for later, mark as rejected
2789
+ setInterruptData(null);
2790
+ clearInterruptMessage('reject'); // Pass 'reject' to show "거부됨"
2791
+ // Focus on input for user to provide feedback
2792
+ const textarea = document.querySelector('.jp-agent-input');
2793
+ if (textarea) {
2794
+ textarea.focus();
2795
+ }
2796
+ return;
2797
+ }
2020
2798
  await resumeFromInterrupt(interruptData, decision);
2021
2799
  };
2022
2800
  const handleKeyDown = (e) => {
@@ -2143,9 +2921,6 @@ SyntaxError: '(' was never closed
2143
2921
  if (normalized.includes('resuming execution')) {
2144
2922
  return '승인 반영 후 실행 재개 중...';
2145
2923
  }
2146
- if (normalized.includes('tool')) {
2147
- return '도구 실행 중';
2148
- }
2149
2924
  return raw;
2150
2925
  };
2151
2926
  const getStatusText = () => {
@@ -2162,6 +2937,7 @@ SyntaxError: '(' was never closed
2162
2937
  return null;
2163
2938
  };
2164
2939
  const statusText = getStatusText();
2940
+ const hasActiveTodos = todos.some(todo => todo.status === 'pending' || todo.status === 'in_progress');
2165
2941
  return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-panel" },
2166
2942
  showSettings && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement(_SettingsPanel__WEBPACK_IMPORTED_MODULE_3__.SettingsPanel, { onClose: () => setShowSettings(false), onSave: handleSaveConfig, currentConfig: llmConfig || undefined })),
2167
2943
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-header" },
@@ -2193,30 +2969,51 @@ SyntaxError: '(' was never closed
2193
2969
  if (isChatMessage(msg)) {
2194
2970
  // 일반 Chat 메시지
2195
2971
  const isAssistant = msg.role === 'assistant';
2972
+ const isShellOutput = msg.metadata?.kind === 'shell-output';
2973
+ const interruptAction = msg.metadata?.interrupt?.action;
2974
+ const isWriteFile = interruptAction === 'write_file_tool';
2975
+ const writePath = (isWriteFile
2976
+ && typeof msg.metadata?.interrupt?.args?.path === 'string') ? msg.metadata?.interrupt?.args?.path : '';
2977
+ const headerRole = msg.role === 'user'
2978
+ ? '사용자'
2979
+ : msg.role === 'system'
2980
+ ? (isShellOutput ? 'shell 실행' : '승인 요청')
2981
+ : 'Agent';
2196
2982
  return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { key: msg.id, className: isAssistant
2197
2983
  ? 'jp-agent-message jp-agent-message-assistant-inline'
2198
- : `jp-agent-message jp-agent-message-${msg.role}` },
2984
+ : `jp-agent-message jp-agent-message-${msg.role}${isShellOutput ? ' jp-agent-message-shell-output' : ''}` },
2199
2985
  !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'),
2986
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-message-role" }, headerRole),
2201
2987
  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" },
2988
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: `jp-agent-message-content${streamingMessageId === msg.id ? ' streaming' : ''}${isShellOutput ? ' jp-agent-message-content-shell' : ''}` }, msg.role === 'system' && msg.metadata?.interrupt ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-interrupt-inline" },
2203
2989
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-interrupt-description" }, msg.content),
2204
2990
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-interrupt-action" },
2205
2991
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-interrupt-action-args" }, (() => {
2992
+ const command = msg.metadata?.interrupt?.args?.command;
2206
2993
  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...' : '';
2994
+ const snippet = (command || code || '(no details)');
2995
+ const language = command ? 'bash' : 'python';
2210
2996
  const resolved = msg.metadata?.interrupt?.resolved;
2997
+ const decision = msg.metadata?.interrupt?.decision;
2998
+ const resolvedText = decision === 'reject' ? '거부됨' : '승인됨';
2999
+ const resolvedClass = decision === 'reject' ? 'jp-agent-interrupt-actions--rejected' : 'jp-agent-interrupt-actions--resolved';
2211
3000
  const actionHtml = resolved
2212
- ? '<div class="jp-agent-interrupt-actions jp-agent-interrupt-actions--resolved">승인됨</div>'
3001
+ ? `<div class="jp-agent-interrupt-actions ${resolvedClass}">${resolvedText}</div>`
2213
3002
  : `
2214
3003
  <div class="code-block-actions jp-agent-interrupt-actions">
2215
3004
  <button class="jp-agent-interrupt-approve-btn" data-action="approve">승인</button>
2216
3005
  <button class="jp-agent-interrupt-reject-btn" data-action="reject">거부</button>
2217
3006
  </div>
2218
3007
  `;
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) => {
3008
+ const renderedHtml = (() => {
3009
+ let html = (0,_utils_markdownRenderer__WEBPACK_IMPORTED_MODULE_7__.formatMarkdownToHtml)(`\n\`\`\`${language}\n${snippet}\n\`\`\``);
3010
+ if (isWriteFile && writePath) {
3011
+ const safePath = escapeHtml(writePath);
3012
+ html = html.replace(/<span class="code-block-language">[^<]*<\/span>/, `<span class="code-block-language jp-agent-interrupt-path">${safePath}</span>`);
3013
+ }
3014
+ return html.replace('</div>', `${actionHtml}</div>`);
3015
+ })();
3016
+ return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-RenderedHTMLCommon", style: { padding: '0 4px' }, dangerouslySetInnerHTML: { __html: renderedHtml }, onClick: (event) => {
2220
3017
  const target = event.target;
2221
3018
  const action = target?.getAttribute?.('data-action');
2222
3019
  if (msg.metadata?.interrupt?.resolved) {
@@ -2276,11 +3073,6 @@ SyntaxError: '(' was never closed
2276
3073
  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
3074
  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
3075
  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
3076
  todos.length > 0 && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-todo-compact" },
2285
3077
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-todo-compact-header", onClick: () => setIsTodoExpanded(!isTodoExpanded) },
2286
3078
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-todo-compact-left" },
@@ -2288,15 +3080,26 @@ SyntaxError: '(' was never closed
2288
3080
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("path", { d: "M6 12l4-4-4-4" })),
2289
3081
  (() => {
2290
3082
  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"));
3083
+ const isStillWorking = isStreaming || isLoading || isAgentRunning;
3084
+ if (currentTodo) {
3085
+ return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null,
3086
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-todo-compact-spinner" }),
3087
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-todo-compact-current" }, currentTodo.content)));
3088
+ }
3089
+ else if (isStillWorking) {
3090
+ return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement((react__WEBPACK_IMPORTED_MODULE_0___default().Fragment), null,
3091
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-todo-compact-spinner" }),
3092
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-todo-compact-current" }, "\uC791\uC5C5 \uB9C8\uBB34\uB9AC \uC911...")));
3093
+ }
3094
+ else {
3095
+ return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-todo-compact-current" }, "\u2713 \uBAA8\uB4E0 \uC791\uC5C5 \uC644\uB8CC"));
3096
+ }
2294
3097
  })()),
2295
3098
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-todo-compact-progress" },
2296
3099
  todos.filter(t => t.status === 'completed').length,
2297
3100
  "/",
2298
3101
  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' : ''}` },
3102
+ 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
3103
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-debug-content" },
2301
3104
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-debug-branch", "aria-hidden": "true" }),
2302
3105
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-debug-text" }, statusText),
@@ -2308,12 +3111,18 @@ SyntaxError: '(' was never closed
2308
3111
  todo.status === 'in_progress' && react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-todo-item-spinner" }),
2309
3112
  todo.status === 'pending' && react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-todo-item-number" }, index + 1)),
2310
3113
  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)))))))),
3114
+ 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' : ''}` },
3115
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-debug-content" },
3116
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-debug-text" }, statusText),
3117
+ !statusText.startsWith('오류:') && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-debug-ellipsis", "aria-hidden": "true" }))))),
2311
3118
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-input-container" },
2312
3119
  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 ? '실행 중...' : '전송')),
3120
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("textarea", { className: `jp-agent-input ${inputMode === 'agent' ? 'jp-agent-input--agent-mode' : ''} ${isRejectionMode ? 'jp-agent-input--rejection-mode' : ''}`, value: input, onChange: (e) => setInput(e.target.value), onKeyDown: handleKeyDown, placeholder: isRejectionMode
3121
+ ? '다른 방향 제시'
3122
+ : (inputMode === 'agent'
3123
+ ? '노트북 작업을 입력하세요... (예: 데이터 시각화 해줘)'
3124
+ : '메시지를 입력하세요...'), rows: 3, disabled: isLoading || isAgentRunning }),
3125
+ 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
3126
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-mode-bar" },
2318
3127
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-mode-toggle-container" },
2319
3128
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", { className: `jp-agent-mode-toggle ${inputMode === 'agent' ? 'jp-agent-mode-toggle--active' : ''}`, onClick: toggleMode, title: `${inputMode === 'agent' ? 'Agent' : 'Chat'} 모드 (⇧Tab)` },
@@ -2830,6 +3639,8 @@ const SettingsPanel = ({ onClose, onSave, currentConfig }) => {
2830
3639
  const [openaiApiKey, setOpenaiApiKey] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(initConfig.openai?.apiKey || '');
2831
3640
  const [openaiModel, setOpenaiModel] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(initConfig.openai?.model || 'gpt-4');
2832
3641
  const [systemPrompt, setSystemPrompt] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(initConfig.systemPrompt || '');
3642
+ const [workspaceRoot, setWorkspaceRoot] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(initConfig.workspaceRoot || '');
3643
+ const [autoApprove, setAutoApprove] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(Boolean(initConfig.autoApprove));
2833
3644
  // Update state when currentConfig changes
2834
3645
  (0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
2835
3646
  if (currentConfig) {
@@ -2851,6 +3662,8 @@ const SettingsPanel = ({ onClose, onSave, currentConfig }) => {
2851
3662
  setOpenaiApiKey(currentConfig.openai?.apiKey || '');
2852
3663
  setOpenaiModel(currentConfig.openai?.model || 'gpt-4');
2853
3664
  setSystemPrompt(currentConfig.systemPrompt || (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_1__.getDefaultLLMConfig)().systemPrompt || '');
3665
+ setWorkspaceRoot(currentConfig.workspaceRoot || '');
3666
+ setAutoApprove(Boolean(currentConfig.autoApprove));
2854
3667
  }
2855
3668
  }, [currentConfig]);
2856
3669
  // Helper: Build LLM config from state
@@ -2870,7 +3683,9 @@ const SettingsPanel = ({ onClose, onSave, currentConfig }) => {
2870
3683
  apiKey: openaiApiKey,
2871
3684
  model: openaiModel
2872
3685
  },
2873
- systemPrompt: systemPrompt && systemPrompt.trim() ? systemPrompt : undefined
3686
+ workspaceRoot: workspaceRoot.trim() ? workspaceRoot.trim() : undefined,
3687
+ systemPrompt: systemPrompt && systemPrompt.trim() ? systemPrompt : undefined,
3688
+ autoApprove
2874
3689
  });
2875
3690
  // Handlers for multiple API keys
2876
3691
  const handleAddKey = () => {
@@ -2964,6 +3779,11 @@ const SettingsPanel = ({ onClose, onSave, currentConfig }) => {
2964
3779
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("option", { value: "gemini" }, "Google Gemini"),
2965
3780
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("option", { value: "vllm" }, "vLLM"),
2966
3781
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("option", { value: "openai" }, "OpenAI"))),
3782
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-settings-group" },
3783
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", { className: "jp-agent-settings-label", htmlFor: "jp-agent-workspace-root" },
3784
+ "\uC6CC\uD06C\uC2A4\uD398\uC774\uC2A4 \uB8E8\uD2B8",
3785
+ 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")),
3786
+ 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
3787
  provider === 'gemini' && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-settings-provider" },
2968
3788
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("h3", null, "Gemini \uC124\uC815"),
2969
3789
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-settings-group" },
@@ -3028,6 +3848,11 @@ const SettingsPanel = ({ onClose, onSave, currentConfig }) => {
3028
3848
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("option", { value: "gpt-4" }, "GPT-4"),
3029
3849
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("option", { value: "gpt-4-turbo" }, "GPT-4 Turbo"),
3030
3850
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("option", { value: "gpt-3.5-turbo" }, "GPT-3.5 Turbo"))))),
3851
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-settings-group" },
3852
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", { className: "jp-agent-settings-label" }, "\uC790\uB3D9 \uC2E4\uD589 \uC2B9\uC778"),
3853
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", { className: "jp-agent-settings-checkbox" },
3854
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("input", { type: "checkbox", checked: autoApprove, onChange: (e) => setAutoApprove(e.target.checked), "data-testid": "auto-approve-checkbox" }),
3855
+ react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null, "\uC2B9\uC778 \uC5C6\uC774 \uBC14\uB85C \uC2E4\uD589 (\uCF54\uB4DC/\uD30C\uC77C/\uC178 \uD3EC\uD568)"))),
3031
3856
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-settings-group" },
3032
3857
  react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", { className: "jp-agent-settings-label" },
3033
3858
  "System Prompt (LangChain)",
@@ -6908,34 +7733,38 @@ __webpack_require__.r(__webpack_exports__);
6908
7733
  */
6909
7734
  const STORAGE_KEY = 'hdsp-agent-llm-config';
6910
7735
  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.
7736
+ Your role is to help users with data analysis, visualization, and Python coding tasks in Jupyter notebooks. You can use only Korean
6912
7737
 
6913
7738
  ## ⚠️ CRITICAL RULE: NEVER produce an empty response
6914
7739
 
6915
7740
  You MUST ALWAYS call a tool in every response. After any tool result, you MUST:
6916
7741
  1. Check your todo list - are there pending or in_progress items?
6917
7742
  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
7743
+ 3. When you suggest next steps for todo item '다음 단계 제시', you MUST create next steps in json format matching this schema:
7744
+ {
7745
+ "next_items": [
7746
+ {
7747
+ "subject": "<subject for next step>",
7748
+ "description": "<detailed description for the next step>"
7749
+ }, ...
7750
+ ]
7751
+ }
7752
+ 4. If ALL todos are completed → call final_answer_tool with a summary
6919
7753
 
6920
7754
  NEVER end your turn without calling a tool. NEVER produce an empty response.
6921
7755
 
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
7756
+ ## 🔴 MANDATORY: Resource Check Before Data Hanlding
7757
+ **ALWAYS call check_resource_tool FIRST** when the task involves:
7758
+ - Loading files: .csv, .parquet, .json, .xlsx, .pickle, .h5, .feather
7759
+ - Handling datasets(dataframe) with pandas, polars, dask, or similar libraries
7760
+ - Training ML models on data files
6932
7761
 
6933
7762
  ## Mandatory Workflow
6934
7763
  1. After EVERY tool result, immediately call the next tool
6935
7764
  2. Continue until ALL todos show status: "completed"
6936
7765
  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
7766
+ 4. Only use jupyter_cell_tool for Python code or when the user explicitly asks to run in a notebook cell
7767
+ 5. For plots and charts, use English text only.
6939
7768
 
6940
7769
  ## ❌ FORBIDDEN (will break the workflow)
6941
7770
  - Producing an empty response (no tool call, no content)
@@ -7027,7 +7856,8 @@ function getDefaultLLMConfig() {
7027
7856
  endpoint: 'http://localhost:8000',
7028
7857
  model: 'default'
7029
7858
  },
7030
- systemPrompt: DEFAULT_LANGCHAIN_SYSTEM_PROMPT
7859
+ systemPrompt: DEFAULT_LANGCHAIN_SYSTEM_PROMPT,
7860
+ autoApprove: false
7031
7861
  };
7032
7862
  }
7033
7863
  /**
@@ -7254,6 +8084,8 @@ __webpack_require__.r(__webpack_exports__);
7254
8084
  class ApiService {
7255
8085
  // 생성자에서 baseUrl을 선택적으로 받도록 하되, 없으면 자동으로 계산
7256
8086
  constructor(baseUrl) {
8087
+ this.resourceUsageCache = null;
8088
+ this.resourceUsageCacheMs = 15000;
7257
8089
  if (baseUrl) {
7258
8090
  this.baseUrl = baseUrl;
7259
8091
  }
@@ -7293,6 +8125,35 @@ class ApiService {
7293
8125
  'X-XSRFToken': this.getCsrfToken()
7294
8126
  };
7295
8127
  }
8128
+ async getResourceUsageSnapshot() {
8129
+ const now = Date.now();
8130
+ if (this.resourceUsageCache
8131
+ && now - this.resourceUsageCache.timestamp < this.resourceUsageCacheMs) {
8132
+ return this.resourceUsageCache.resource;
8133
+ }
8134
+ try {
8135
+ const response = await fetch(`${this.baseUrl}/resource-usage`, {
8136
+ method: 'GET',
8137
+ credentials: 'include'
8138
+ });
8139
+ if (!response.ok) {
8140
+ return null;
8141
+ }
8142
+ const payload = await response.json().catch(() => ({}));
8143
+ const candidate = payload?.resource ?? payload;
8144
+ const snapshot = candidate && typeof candidate === 'object' && !Array.isArray(candidate)
8145
+ ? candidate
8146
+ : null;
8147
+ if (snapshot) {
8148
+ this.resourceUsageCache = { resource: snapshot, timestamp: now };
8149
+ }
8150
+ return snapshot;
8151
+ }
8152
+ catch (error) {
8153
+ console.warn('[ApiService] Resource usage fetch failed:', error);
8154
+ return null;
8155
+ }
8156
+ }
7296
8157
  // ═══════════════════════════════════════════════════════════════════════════
7297
8158
  // Global Rate Limit Handling with Key Rotation
7298
8159
  // ═══════════════════════════════════════════════════════════════════════════
@@ -7401,7 +8262,9 @@ class ApiService {
7401
8262
  * - Server receives ONLY ONE key per request
7402
8263
  * - On 429 rate limit, frontend rotates key and retries with next key
7403
8264
  */
7404
- async sendMessageStream(request, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall) {
8265
+ async sendMessageStream(request, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall, onComplete, // Callback to capture thread_id for context persistence
8266
+ threadId // Optional thread_id to continue existing conversation
8267
+ ) {
7405
8268
  // Maximum retry attempts (should match number of keys)
7406
8269
  const MAX_RETRIES = 10;
7407
8270
  let currentConfig = request.llmConfig;
@@ -7412,7 +8275,7 @@ class ApiService {
7412
8275
  ? { ...request, llmConfig: (0,_ApiKeyManager__WEBPACK_IMPORTED_MODULE_0__.buildSingleKeyConfig)(currentConfig) }
7413
8276
  : request;
7414
8277
  try {
7415
- await this.sendMessageStreamInternal(requestToSend, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall);
8278
+ await this.sendMessageStreamInternal(requestToSend, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall, onComplete, threadId);
7416
8279
  // Success - reset key rotation state
7417
8280
  (0,_ApiKeyManager__WEBPACK_IMPORTED_MODULE_0__.resetKeyRotation)();
7418
8281
  return;
@@ -7462,11 +8325,16 @@ class ApiService {
7462
8325
  * Internal streaming implementation (without retry logic)
7463
8326
  * Uses LangChain agent endpoint for improved middleware support
7464
8327
  */
7465
- async sendMessageStreamInternal(request, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall) {
8328
+ async sendMessageStreamInternal(request, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall, onComplete, threadId) {
7466
8329
  // Convert IChatRequest to LangChain AgentRequest format
7467
8330
  // Frontend's context has limited fields, map what's available
8331
+ const resourceContext = await this.getResourceUsageSnapshot();
8332
+ const requestConfig = resourceContext && request.llmConfig
8333
+ ? { ...request.llmConfig, resourceContext }
8334
+ : request.llmConfig;
7468
8335
  const langchainRequest = {
7469
8336
  request: request.message,
8337
+ threadId: threadId,
7470
8338
  notebookContext: request.context ? {
7471
8339
  notebook_path: request.context.notebookPath,
7472
8340
  cell_count: 0,
@@ -7474,15 +8342,22 @@ class ApiService {
7474
8342
  defined_variables: [],
7475
8343
  recent_cells: request.context.selectedCells?.map(cell => ({ source: cell })) || []
7476
8344
  } : undefined,
7477
- llmConfig: request.llmConfig,
8345
+ llmConfig: requestConfig,
7478
8346
  workspaceRoot: '.'
7479
8347
  };
8348
+ // Debug: log request size
8349
+ const requestBody = JSON.stringify(langchainRequest);
8350
+ console.log('[ApiService] Sending langchain request:', {
8351
+ messageLength: request.message.length,
8352
+ bodyLength: requestBody.length,
8353
+ threadId: threadId,
8354
+ });
7480
8355
  // Use LangChain streaming endpoint
7481
8356
  const response = await fetch(`${this.baseUrl}/agent/langchain/stream`, {
7482
8357
  method: 'POST',
7483
8358
  headers: this.getHeaders(),
7484
8359
  credentials: 'include',
7485
- body: JSON.stringify(langchainRequest)
8360
+ body: requestBody
7486
8361
  });
7487
8362
  if (!response.ok) {
7488
8363
  const error = await response.text();
@@ -7529,6 +8404,15 @@ class ApiService {
7529
8404
  if (onDebugClear) {
7530
8405
  onDebugClear();
7531
8406
  }
8407
+ // Capture thread_id for context persistence across cycles
8408
+ console.log('[ApiService] Complete event received, data:', data);
8409
+ if (onComplete && data.thread_id) {
8410
+ console.log('[ApiService] Calling onComplete with threadId:', data.thread_id);
8411
+ onComplete({ threadId: data.thread_id });
8412
+ }
8413
+ else {
8414
+ console.log('[ApiService] onComplete not called - onComplete:', !!onComplete, 'thread_id:', data.thread_id);
8415
+ }
7532
8416
  return;
7533
8417
  }
7534
8418
  // Handle errors
@@ -7561,7 +8445,9 @@ class ApiService {
7561
8445
  onToolCall({
7562
8446
  tool: data.tool,
7563
8447
  code: data.code,
7564
- content: data.content
8448
+ content: data.content,
8449
+ command: data.command,
8450
+ timeout: data.timeout
7565
8451
  });
7566
8452
  currentEventType = '';
7567
8453
  continue;
@@ -7612,6 +8498,10 @@ class ApiService {
7612
8498
  * Resume interrupted agent execution with user decision
7613
8499
  */
7614
8500
  async resumeAgent(threadId, decision, args, feedback, llmConfig, onChunk, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall) {
8501
+ const resourceContext = await this.getResourceUsageSnapshot();
8502
+ const requestConfig = resourceContext && llmConfig
8503
+ ? { ...llmConfig, resourceContext }
8504
+ : llmConfig;
7615
8505
  const resumeRequest = {
7616
8506
  threadId,
7617
8507
  decisions: [{
@@ -7619,7 +8509,7 @@ class ApiService {
7619
8509
  args,
7620
8510
  feedback
7621
8511
  }],
7622
- llmConfig,
8512
+ llmConfig: requestConfig,
7623
8513
  workspaceRoot: '.'
7624
8514
  };
7625
8515
  const response = await fetch(`${this.baseUrl}/agent/langchain/resume`, {
@@ -7704,7 +8594,9 @@ class ApiService {
7704
8594
  onToolCall({
7705
8595
  tool: data.tool,
7706
8596
  code: data.code,
7707
- content: data.content
8597
+ content: data.content,
8598
+ command: data.command,
8599
+ timeout: data.timeout
7708
8600
  });
7709
8601
  currentEventType = '';
7710
8602
  continue;
@@ -7724,6 +8616,194 @@ class ApiService {
7724
8616
  reader.releaseLock();
7725
8617
  }
7726
8618
  }
8619
+ async executeCommand(command, timeout, cwd, stdin) {
8620
+ const response = await fetch(`${this.baseUrl}/execute-command`, {
8621
+ method: 'POST',
8622
+ headers: this.getHeaders(),
8623
+ credentials: 'include',
8624
+ body: JSON.stringify({ command, timeout, cwd, stdin })
8625
+ });
8626
+ const payload = await response.json().catch(() => ({}));
8627
+ if (!response.ok) {
8628
+ const errorMessage = payload.error || 'Failed to execute command';
8629
+ throw new Error(errorMessage);
8630
+ }
8631
+ return payload;
8632
+ }
8633
+ async executeCommandStream(command, options) {
8634
+ const response = await fetch(`${this.baseUrl}/execute-command/stream`, {
8635
+ method: 'POST',
8636
+ headers: this.getHeaders(),
8637
+ credentials: 'include',
8638
+ body: JSON.stringify({
8639
+ command,
8640
+ timeout: options?.timeout,
8641
+ cwd: options?.cwd,
8642
+ stdin: options?.stdin
8643
+ })
8644
+ });
8645
+ if (!response.ok) {
8646
+ const payload = await response.json().catch(() => ({}));
8647
+ const errorMessage = payload.error || 'Failed to execute command';
8648
+ throw new Error(errorMessage);
8649
+ }
8650
+ const reader = response.body?.getReader();
8651
+ if (!reader) {
8652
+ throw new Error('Response body is not readable');
8653
+ }
8654
+ const decoder = new TextDecoder();
8655
+ let buffer = '';
8656
+ let result = null;
8657
+ let streamError = null;
8658
+ try {
8659
+ while (true) {
8660
+ const { done, value } = await reader.read();
8661
+ if (done)
8662
+ break;
8663
+ buffer += decoder.decode(value, { stream: true });
8664
+ const lines = buffer.split('\n');
8665
+ buffer = lines.pop() || '';
8666
+ let currentEventType = '';
8667
+ for (const line of lines) {
8668
+ if (line.startsWith('event: ')) {
8669
+ currentEventType = line.slice(7).trim();
8670
+ continue;
8671
+ }
8672
+ if (!line.startsWith('data: ')) {
8673
+ continue;
8674
+ }
8675
+ let data;
8676
+ try {
8677
+ data = JSON.parse(line.slice(6));
8678
+ }
8679
+ catch (e) {
8680
+ continue;
8681
+ }
8682
+ if (currentEventType === 'output') {
8683
+ if (typeof data.text === 'string' && options?.onOutput) {
8684
+ options.onOutput({
8685
+ stream: data.stream === 'stderr' ? 'stderr' : 'stdout',
8686
+ text: data.text
8687
+ });
8688
+ }
8689
+ currentEventType = '';
8690
+ continue;
8691
+ }
8692
+ if (currentEventType === 'error') {
8693
+ streamError = data.error || 'Command execution failed';
8694
+ currentEventType = '';
8695
+ continue;
8696
+ }
8697
+ if (currentEventType === 'result') {
8698
+ result = data;
8699
+ return result;
8700
+ }
8701
+ currentEventType = '';
8702
+ }
8703
+ }
8704
+ }
8705
+ finally {
8706
+ reader.releaseLock();
8707
+ }
8708
+ if (streamError) {
8709
+ throw new Error(streamError);
8710
+ }
8711
+ if (!result) {
8712
+ throw new Error('No command result received');
8713
+ }
8714
+ return result;
8715
+ }
8716
+ async writeFile(path, content, options) {
8717
+ const response = await fetch(`${this.baseUrl}/write-file`, {
8718
+ method: 'POST',
8719
+ headers: this.getHeaders(),
8720
+ credentials: 'include',
8721
+ body: JSON.stringify({
8722
+ path,
8723
+ content,
8724
+ encoding: options?.encoding,
8725
+ overwrite: options?.overwrite,
8726
+ cwd: options?.cwd
8727
+ })
8728
+ });
8729
+ const payload = await response.json().catch(() => ({}));
8730
+ if (!response.ok) {
8731
+ const errorMessage = payload.error || 'Failed to write file';
8732
+ throw new Error(errorMessage);
8733
+ }
8734
+ return payload;
8735
+ }
8736
+ /**
8737
+ * Search workspace for files matching pattern
8738
+ * Executed on Jupyter server using grep/ripgrep
8739
+ */
8740
+ async searchWorkspace(options) {
8741
+ const response = await fetch(`${this.baseUrl}/search-workspace`, {
8742
+ method: 'POST',
8743
+ headers: this.getHeaders(),
8744
+ credentials: 'include',
8745
+ body: JSON.stringify({
8746
+ pattern: options.pattern,
8747
+ file_types: options.file_types || ['*.py', '*.ipynb'],
8748
+ path: options.path || '.',
8749
+ max_results: options.max_results || 50,
8750
+ case_sensitive: options.case_sensitive || false
8751
+ })
8752
+ });
8753
+ const payload = await response.json().catch(() => ({}));
8754
+ if (!response.ok) {
8755
+ const errorMessage = payload.error || 'Failed to search workspace';
8756
+ throw new Error(errorMessage);
8757
+ }
8758
+ return payload;
8759
+ }
8760
+ /**
8761
+ * Search notebook cells for pattern
8762
+ * Executed on Jupyter server
8763
+ */
8764
+ async searchNotebookCells(options) {
8765
+ const response = await fetch(`${this.baseUrl}/search-notebook-cells`, {
8766
+ method: 'POST',
8767
+ headers: this.getHeaders(),
8768
+ credentials: 'include',
8769
+ body: JSON.stringify({
8770
+ pattern: options.pattern,
8771
+ notebook_path: options.notebook_path,
8772
+ cell_type: options.cell_type,
8773
+ max_results: options.max_results || 30,
8774
+ case_sensitive: options.case_sensitive || false
8775
+ })
8776
+ });
8777
+ const payload = await response.json().catch(() => ({}));
8778
+ if (!response.ok) {
8779
+ const errorMessage = payload.error || 'Failed to search notebook cells';
8780
+ throw new Error(errorMessage);
8781
+ }
8782
+ return payload;
8783
+ }
8784
+ /**
8785
+ * Check system resources and file sizes before data processing
8786
+ * Executed on Jupyter server
8787
+ */
8788
+ async checkResource(options) {
8789
+ const response = await fetch(`${this.baseUrl}/check-resource`, {
8790
+ method: 'POST',
8791
+ headers: this.getHeaders(),
8792
+ credentials: 'include',
8793
+ body: JSON.stringify({
8794
+ files: options.files || [],
8795
+ dataframes: options.dataframes || [],
8796
+ file_size_command: options.file_size_command || '',
8797
+ dataframe_check_code: options.dataframe_check_code || ''
8798
+ })
8799
+ });
8800
+ const payload = await response.json().catch(() => ({}));
8801
+ if (!response.ok) {
8802
+ const errorMessage = payload.error || 'Failed to check resources';
8803
+ throw new Error(errorMessage);
8804
+ }
8805
+ return payload;
8806
+ }
7727
8807
  /**
7728
8808
  * Save configuration
7729
8809
  */
@@ -9238,9 +10318,9 @@ class ToolExecutor {
9238
10318
  },
9239
10319
  });
9240
10320
  }
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')) {
10321
+ // execute_command_tool 도구 등록 (조건부 승인)
10322
+ const executeCommandDef = _ToolRegistry__WEBPACK_IMPORTED_MODULE_1__.BUILTIN_TOOL_DEFINITIONS.find(t => t.name === 'execute_command_tool');
10323
+ if (executeCommandDef && !this.registry.hasTool('execute_command_tool')) {
9244
10324
  this.registry.register({
9245
10325
  ...executeCommandDef,
9246
10326
  executor: async (params, context) => {
@@ -9620,6 +10700,11 @@ class ToolExecutor {
9620
10700
  isDangerousCommand(command) {
9621
10701
  return _ToolRegistry__WEBPACK_IMPORTED_MODULE_1__.DANGEROUS_COMMAND_PATTERNS.some(pattern => pattern.test(command));
9622
10702
  }
10703
+ summarizeOutput(output, maxLines = 2) {
10704
+ const lines = output.split(/\r?\n/).filter(line => line.length > 0);
10705
+ const text = lines.slice(0, maxLines).join('\n');
10706
+ return { text, truncated: lines.length > maxLines };
10707
+ }
9623
10708
  /**
9624
10709
  * read_file 도구: 파일 읽기
9625
10710
  */
@@ -10037,19 +11122,19 @@ print(json.dumps(result))
10037
11122
  }
10038
11123
  }
10039
11124
  /**
10040
- * execute_command 도구: 셸 명령 실행 (조건부 승인)
11125
+ * execute_command_tool 도구: 셸 명령 실행 (조건부 승인)
10041
11126
  */
10042
11127
  async executeCommand(params, context) {
10043
11128
  console.log('[ToolExecutor] executeCommand:', params.command);
10044
- const timeout = params.timeout || 30000;
11129
+ const timeout = typeof params.timeout === 'number' ? params.timeout : 600000;
10045
11130
  // 위험 명령 검사 및 조건부 승인 요청
10046
11131
  if (this.isDangerousCommand(params.command)) {
10047
11132
  console.log('[ToolExecutor] Dangerous command detected, requesting approval');
10048
11133
  // 승인 요청
10049
11134
  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'),
11135
+ id: `execute_command_tool-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
11136
+ toolName: 'execute_command_tool',
11137
+ toolDefinition: this.registry.getTool('execute_command_tool'),
10053
11138
  parameters: params,
10054
11139
  stepNumber: context.stepNumber,
10055
11140
  description: `🔴 위험 명령 실행 요청:\n\n\`${params.command}\`\n\n이 명령은 시스템에 영향을 줄 수 있습니다.`,
@@ -10066,56 +11151,32 @@ print(json.dumps(result))
10066
11151
  }
10067
11152
  }
10068
11153
  }
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();
11154
+ if (!this.apiService) {
11155
+ return { success: false, error: 'ApiService not available for execute_command_tool' };
11156
+ }
10098
11157
  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
- }
11158
+ const result = await this.apiService.executeCommandStream(params.command, { timeout });
11159
+ const stdout = typeof result.stdout === 'string' ? result.stdout : '';
11160
+ const stderr = typeof result.stderr === 'string' ? result.stderr : '';
11161
+ const combined = [stdout, stderr].filter(Boolean).join('\n');
11162
+ const summary = this.summarizeOutput(combined, 2);
11163
+ const output = summary.text || '(no output)';
11164
+ if (result.success) {
11165
+ return {
11166
+ success: true,
11167
+ output,
11168
+ };
10114
11169
  }
10115
- return { success: false, error: execResult.error?.evalue || 'Command execution failed' };
11170
+ const errorText = summary.text || result.error || stderr || `Command failed with code ${result.returncode}`;
11171
+ return {
11172
+ success: false,
11173
+ error: errorText,
11174
+ };
10116
11175
  }
10117
11176
  catch (error) {
10118
- return { success: false, error: error.message };
11177
+ const message = error instanceof Error ? error.message : 'Command execution failed';
11178
+ const summary = this.summarizeOutput(String(message), 2);
11179
+ return { success: false, error: summary.text || 'Command execution failed' };
10119
11180
  }
10120
11181
  }
10121
11182
  /**
@@ -11901,7 +12962,7 @@ const BUILTIN_TOOL_DEFINITIONS = [
11901
12962
  // 확장 도구 (시스템)
11902
12963
  // ─────────────────────────────────────────────────────────────────────────
11903
12964
  {
11904
- name: 'execute_command',
12965
+ name: 'execute_command_tool',
11905
12966
  description: '셸 명령 실행 (위험 명령만 승인 필요)',
11906
12967
  riskLevel: 'critical',
11907
12968
  requiresApproval: false,
@@ -11986,7 +13047,7 @@ const BUILTIN_TOOL_DEFINITIONS = [
11986
13047
  ];
11987
13048
  /**
11988
13049
  * 위험한 셸 명령 패턴들
11989
- * execute_command에서 이 패턴에 매칭되는 명령은 사용자 승인 필요
13050
+ * execute_command_tool에서 이 패턴에 매칭되는 명령은 사용자 승인 필요
11990
13051
  */
11991
13052
  const DANGEROUS_COMMAND_PATTERNS = [
11992
13053
  // 파일 삭제/제거
@@ -12515,6 +13576,154 @@ function normalizeIndentation(code) {
12515
13576
  });
12516
13577
  return normalized.join('\n');
12517
13578
  }
13579
+ function extractNextItemsBlock(text) {
13580
+ if (!text.trim())
13581
+ return null;
13582
+ const fencedRegex = /```(\w+)?\n([\s\S]*?)```/g;
13583
+ let match;
13584
+ while ((match = fencedRegex.exec(text)) !== null) {
13585
+ const lang = (match[1] || '').toLowerCase();
13586
+ const content = match[2].trim();
13587
+ if (!content)
13588
+ continue;
13589
+ if (lang && lang !== 'json' && !content.includes('"next_items"')) {
13590
+ continue;
13591
+ }
13592
+ const items = parseNextItemsPayload(content);
13593
+ if (items) {
13594
+ const placeholder = `__NEXT_ITEMS_${Math.random().toString(36).slice(2, 11)}__`;
13595
+ const updated = text.slice(0, match.index) + placeholder + text.slice(match.index + match[0].length);
13596
+ return { items, placeholder, text: updated };
13597
+ }
13598
+ }
13599
+ const range = findNextItemsJsonRange(text);
13600
+ if (!range)
13601
+ return null;
13602
+ const candidate = text.slice(range.start, range.end + 1);
13603
+ const items = parseNextItemsPayload(candidate);
13604
+ if (!items)
13605
+ return null;
13606
+ let replacementStart = range.start;
13607
+ const lineStart = text.lastIndexOf('\n', range.start - 1) + 1;
13608
+ const linePrefix = text.slice(lineStart, range.start).trim();
13609
+ const normalizedPrefix = linePrefix.replace(/[::]$/, '').toLowerCase();
13610
+ if (normalizedPrefix === 'json') {
13611
+ replacementStart = lineStart;
13612
+ }
13613
+ const placeholder = `__NEXT_ITEMS_${Math.random().toString(36).slice(2, 11)}__`;
13614
+ const updated = text.slice(0, replacementStart) + placeholder + text.slice(range.end + 1);
13615
+ return { items, placeholder, text: updated };
13616
+ }
13617
+ function findNextItemsJsonRange(text) {
13618
+ const key = '"next_items"';
13619
+ let searchIndex = text.indexOf(key);
13620
+ while (searchIndex !== -1) {
13621
+ const start = text.lastIndexOf('{', searchIndex);
13622
+ if (start === -1)
13623
+ return null;
13624
+ const end = findMatchingBrace(text, start);
13625
+ if (end !== -1) {
13626
+ return { start, end };
13627
+ }
13628
+ searchIndex = text.indexOf(key, searchIndex + key.length);
13629
+ }
13630
+ return null;
13631
+ }
13632
+ function findMatchingBrace(text, start) {
13633
+ let depth = 0;
13634
+ let inString = false;
13635
+ let escaped = false;
13636
+ for (let i = start; i < text.length; i++) {
13637
+ const char = text[i];
13638
+ if (inString) {
13639
+ if (escaped) {
13640
+ escaped = false;
13641
+ continue;
13642
+ }
13643
+ if (char === '\\') {
13644
+ escaped = true;
13645
+ continue;
13646
+ }
13647
+ if (char === '"') {
13648
+ inString = false;
13649
+ }
13650
+ continue;
13651
+ }
13652
+ if (char === '"') {
13653
+ inString = true;
13654
+ continue;
13655
+ }
13656
+ if (char === '{') {
13657
+ depth += 1;
13658
+ }
13659
+ else if (char === '}') {
13660
+ depth -= 1;
13661
+ if (depth === 0) {
13662
+ return i;
13663
+ }
13664
+ }
13665
+ }
13666
+ return -1;
13667
+ }
13668
+ function parseNextItemsPayload(payload) {
13669
+ if (!payload.startsWith('{') || !payload.endsWith('}'))
13670
+ return null;
13671
+ try {
13672
+ const parsed = JSON.parse(payload);
13673
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
13674
+ return null;
13675
+ const nextItemsRaw = parsed.next_items;
13676
+ if (!Array.isArray(nextItemsRaw))
13677
+ return null;
13678
+ const items = nextItemsRaw
13679
+ .map((item) => {
13680
+ if (!item || typeof item !== 'object')
13681
+ return null;
13682
+ const subject = typeof item.subject === 'string'
13683
+ ? item.subject.trim()
13684
+ : '';
13685
+ const description = typeof item.description === 'string'
13686
+ ? item.description.trim()
13687
+ : '';
13688
+ if (!subject && !description)
13689
+ return null;
13690
+ return { subject, description };
13691
+ })
13692
+ .filter((item) => Boolean(item));
13693
+ return items.length > 0 ? items : null;
13694
+ }
13695
+ catch {
13696
+ return null;
13697
+ }
13698
+ }
13699
+ function renderNextItemsList(items) {
13700
+ // Simple arrow icon
13701
+ const arrowSvg = `
13702
+ <svg class="jp-next-items-icon" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
13703
+ <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"/>
13704
+ </svg>`;
13705
+ const listItems = items.map((item) => {
13706
+ const subject = escapeHtml(item.subject);
13707
+ const description = escapeHtml(item.description);
13708
+ const subjectHtml = subject ? `<div class="jp-next-items-subject">${subject}</div>` : '';
13709
+ const descriptionHtml = description ? `<div class="jp-next-items-description">${description}</div>` : '';
13710
+ return `
13711
+ <li class="jp-next-items-item" data-next-item="true" role="button" tabindex="0">
13712
+ <div class="jp-next-items-text">
13713
+ ${subjectHtml}
13714
+ ${descriptionHtml}
13715
+ </div>
13716
+ ${arrowSvg}
13717
+ </li>`;
13718
+ }).join('');
13719
+ return `
13720
+ <div class="jp-next-items" data-next-items="true">
13721
+ <div class="jp-next-items-header">다음 단계 제안</div>
13722
+ <ul class="jp-next-items-list" role="list">
13723
+ ${listItems}
13724
+ </ul>
13725
+ </div>`;
13726
+ }
12518
13727
  /**
12519
13728
  * Highlight Python code with inline styles
12520
13729
  */
@@ -12915,9 +14124,10 @@ function parseMarkdownTable(tableText) {
12915
14124
  * Format markdown text to HTML with syntax highlighting
12916
14125
  */
12917
14126
  function formatMarkdownToHtml(text) {
14127
+ const nextItemsBlock = extractNextItemsBlock(text);
12918
14128
  // Decode HTML entities if present
12919
14129
  const textarea = document.createElement('textarea');
12920
- textarea.innerHTML = text;
14130
+ textarea.innerHTML = nextItemsBlock ? nextItemsBlock.text : text;
12921
14131
  let html = textarea.value;
12922
14132
  // Step 0.5: Protect DataFrame HTML tables (must be before code blocks)
12923
14133
  const dataframeHtmlPlaceholders = [];
@@ -12959,6 +14169,9 @@ function formatMarkdownToHtml(text) {
12959
14169
  '</div>' +
12960
14170
  '</div>' +
12961
14171
  '<pre class="code-block language-' + escapeHtml(lang) + '"><code id="' + blockId + '">' + highlightedCode + '</code></pre>' +
14172
+ '<button class="code-block-toggle" data-block-id="' + blockId + '" title="전체 보기" aria-label="전체 보기" aria-expanded="false">' +
14173
+ '<span class="code-block-toggle-icon" aria-hidden="true">▾</span>' +
14174
+ '</button>' +
12962
14175
  '</div>';
12963
14176
  codeBlockPlaceholders.push({
12964
14177
  placeholder: placeholder,
@@ -13020,7 +14233,7 @@ function formatMarkdownToHtml(text) {
13020
14233
  return '\n' + placeholder + '\n';
13021
14234
  });
13022
14235
  // Step 4: Escape HTML for non-placeholder text
13023
- html = html.split(/(__(?:DATAFRAME_HTML|CODE_BLOCK|INLINE_CODE|TABLE)_[a-z0-9-]+__)/gi)
14236
+ html = html.split(/(__(?:DATAFRAME_HTML|CODE_BLOCK|INLINE_CODE|TABLE|NEXT_ITEMS)_[a-z0-9-]+__)/gi)
13024
14237
  .map((part, index) => {
13025
14238
  // Odd indices are placeholders - keep as is
13026
14239
  if (index % 2 === 1)
@@ -13068,6 +14281,10 @@ function formatMarkdownToHtml(text) {
13068
14281
  codeBlockPlaceholders.forEach(item => {
13069
14282
  html = html.replace(item.placeholder, item.html);
13070
14283
  });
14284
+ // Step 8.5: Restore next items list placeholders
14285
+ if (nextItemsBlock) {
14286
+ html = html.split(nextItemsBlock.placeholder).join(renderNextItemsList(nextItemsBlock.items));
14287
+ }
13071
14288
  return html;
13072
14289
  }
13073
14290
 
@@ -13229,4 +14446,4 @@ __webpack_require__.r(__webpack_exports__);
13229
14446
  /***/ }
13230
14447
 
13231
14448
  }]);
13232
- //# sourceMappingURL=lib_index_js.a223ea20056954479ae9.js.map
14449
+ //# sourceMappingURL=lib_index_js.29cf4312af19e86f82af.js.map