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