hdsp-jupyter-extension 2.0.7__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/retriever.py +13 -8
- agent_server/core/vllm_embedding_service.py +243 -0
- agent_server/langchain/agent.py +8 -0
- agent_server/langchain/custom_middleware.py +58 -31
- agent_server/langchain/hitl_config.py +6 -1
- agent_server/langchain/logging_utils.py +53 -14
- agent_server/langchain/prompts.py +47 -16
- agent_server/langchain/tools/__init__.py +13 -0
- agent_server/langchain/tools/file_tools.py +285 -7
- agent_server/langchain/tools/file_utils.py +334 -0
- agent_server/langchain/tools/lsp_tools.py +264 -0
- agent_server/main.py +7 -0
- agent_server/routers/langchain_agent.py +115 -19
- agent_server/routers/rag.py +8 -3
- hdsp_agent_core/models/rag.py +15 -1
- hdsp_agent_core/services/rag_service.py +6 -1
- {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/build_log.json +1 -1
- {hdsp_jupyter_extension-2.0.7.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/package.json +3 -2
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.4770ec0fb2d173b6deb4.js → hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.8740a527757068814573.js +160 -3
- 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.7.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.29cf4312af19e86f82af.js → hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.e4ff4b5779b5e049f84c.js +1759 -221
- 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.7.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.61343eb4cf0577e74b50.js → hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.020cdb0b864cfaa4e41e.js +14 -12
- hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.020cdb0b864cfaa4e41e.js.map +1 -0
- jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js → hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js +2 -209
- 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 +1 -0
- jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js → 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 +209 -2
- 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 +1 -0
- jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js → hdsp_jupyter_extension-2.0.8.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js +212 -3
- 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 +1 -0
- {hdsp_jupyter_extension-2.0.7.dist-info → hdsp_jupyter_extension-2.0.8.dist-info}/METADATA +1 -1
- {hdsp_jupyter_extension-2.0.7.dist-info → hdsp_jupyter_extension-2.0.8.dist-info}/RECORD +66 -63
- jupyter_ext/__init__.py +18 -0
- jupyter_ext/_version.py +1 -1
- jupyter_ext/handlers.py +176 -1
- jupyter_ext/labextension/build_log.json +1 -1
- jupyter_ext/labextension/package.json +3 -2
- jupyter_ext/labextension/static/{frontend_styles_index_js.4770ec0fb2d173b6deb4.js → frontend_styles_index_js.8740a527757068814573.js} +160 -3
- jupyter_ext/labextension/static/frontend_styles_index_js.8740a527757068814573.js.map +1 -0
- jupyter_ext/labextension/static/{lib_index_js.29cf4312af19e86f82af.js → lib_index_js.e4ff4b5779b5e049f84c.js} +1759 -221
- jupyter_ext/labextension/static/lib_index_js.e4ff4b5779b5e049f84c.js.map +1 -0
- jupyter_ext/labextension/static/{remoteEntry.61343eb4cf0577e74b50.js → remoteEntry.020cdb0b864cfaa4e41e.js} +14 -12
- jupyter_ext/labextension/static/remoteEntry.020cdb0b864cfaa4e41e.js.map +1 -0
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js → jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js +2 -209
- jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js.24edcc52a1c014a8a5f0.js.map +1 -0
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js → jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js +209 -2
- jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.19ecf6babe00caff6b8a.js.map +1 -0
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js → jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js +212 -3
- jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.1f5038488cdfd8b3a85d.js.map +1 -0
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/frontend_styles_index_js.4770ec0fb2d173b6deb4.js.map +0 -1
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/lib_index_js.29cf4312af19e86f82af.js.map +0 -1
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/remoteEntry.61343eb4cf0577e74b50.js.map +0 -1
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js.map +0 -1
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js.map +0 -1
- hdsp_jupyter_extension-2.0.7.data/data/share/jupyter/labextensions/hdsp-agent/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js.map +0 -1
- jupyter_ext/labextension/static/frontend_styles_index_js.4770ec0fb2d173b6deb4.js.map +0 -1
- jupyter_ext/labextension/static/lib_index_js.29cf4312af19e86f82af.js.map +0 -1
- jupyter_ext/labextension/static/remoteEntry.61343eb4cf0577e74b50.js.map +0 -1
- jupyter_ext/labextension/static/vendors-node_modules_emotion_cache_dist_emotion-cache_browser_development_esm_js-node_modules-782ee5.d9ed8645ef1d311657d8.js.map +0 -1
- jupyter_ext/labextension/static/vendors-node_modules_emotion_react_dist_emotion-react_browser_development_esm_js.36b49c71871f98d4f549.js.map +0 -1
- jupyter_ext/labextension/static/vendors-node_modules_mui_material_utils_createSvgIcon_js.2e13df4ea61496e95d45.js.map +0 -1
- {hdsp_jupyter_extension-2.0.7.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.7.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/install.json +0 -0
- {hdsp_jupyter_extension-2.0.7.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.7.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.7.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.7.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.7.data → hdsp_jupyter_extension-2.0.8.data}/data/share/jupyter/labextensions/hdsp-agent/static/style.js +0 -0
- {hdsp_jupyter_extension-2.0.7.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.7.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.7.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.7.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.7.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.7.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.7.dist-info → hdsp_jupyter_extension-2.0.8.dist-info}/WHEEL +0 -0
- {hdsp_jupyter_extension-2.0.7.dist-info → hdsp_jupyter_extension-2.0.8.dist-info}/licenses/LICENSE +0 -0
|
@@ -1088,99 +1088,127 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
|
|
|
1088
1088
|
try {
|
|
1089
1089
|
// Use LLM prompt if available, otherwise use the display content
|
|
1090
1090
|
const messageToSend = llmPrompt || displayContentText;
|
|
1091
|
-
console.log('[AgentPanel] Sending message with agentThreadId:', agentThreadId);
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
// onMetadata callback - update conversationId and metadata
|
|
1105
|
-
(metadata) => {
|
|
1106
|
-
if (metadata.conversationId && !conversationId) {
|
|
1107
|
-
setConversationId(metadata.conversationId);
|
|
1108
|
-
}
|
|
1109
|
-
if (metadata.provider || metadata.model) {
|
|
1091
|
+
console.log('[AgentPanel] Sending message with mode:', inputMode, 'agentThreadId:', agentThreadId);
|
|
1092
|
+
// Chat 모드: 단순 Q&A (sendChatStream)
|
|
1093
|
+
// Agent V2 모드 또는 그 외: LangChain Deep Agent (sendAgentV2Stream)
|
|
1094
|
+
if (inputMode === 'chat') {
|
|
1095
|
+
// 단순 Chat 모드 - /chat/stream 사용
|
|
1096
|
+
await apiService.sendChatStream({
|
|
1097
|
+
message: messageToSend,
|
|
1098
|
+
conversationId: conversationId || undefined,
|
|
1099
|
+
llmConfig: currentConfig
|
|
1100
|
+
},
|
|
1101
|
+
// onChunk callback
|
|
1102
|
+
(chunk) => {
|
|
1103
|
+
streamedContent += chunk;
|
|
1110
1104
|
setMessages(prev => prev.map(msg => msg.id === assistantMessageId && isChatMessage(msg)
|
|
1111
|
-
? {
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1105
|
+
? { ...msg, content: streamedContent }
|
|
1106
|
+
: msg));
|
|
1107
|
+
},
|
|
1108
|
+
// onMetadata callback
|
|
1109
|
+
(metadata) => {
|
|
1110
|
+
if (metadata.conversationId && !conversationId) {
|
|
1111
|
+
setConversationId(metadata.conversationId);
|
|
1112
|
+
}
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
else {
|
|
1116
|
+
// Agent V2 모드 - /agent/langchain/stream 사용 (HITL, Todo, 도구 실행)
|
|
1117
|
+
await apiService.sendAgentV2Stream({
|
|
1118
|
+
message: messageToSend,
|
|
1119
|
+
conversationId: conversationId || undefined,
|
|
1120
|
+
llmConfig: currentConfig // Include API keys with request
|
|
1121
|
+
},
|
|
1122
|
+
// onChunk callback - update message content incrementally
|
|
1123
|
+
(chunk) => {
|
|
1124
|
+
streamedContent += chunk;
|
|
1125
|
+
setMessages(prev => prev.map(msg => msg.id === assistantMessageId && isChatMessage(msg)
|
|
1126
|
+
? { ...msg, content: streamedContent }
|
|
1127
|
+
: msg));
|
|
1128
|
+
},
|
|
1129
|
+
// onMetadata callback - update conversationId and metadata
|
|
1130
|
+
(metadata) => {
|
|
1131
|
+
if (metadata.conversationId && !conversationId) {
|
|
1132
|
+
setConversationId(metadata.conversationId);
|
|
1133
|
+
}
|
|
1134
|
+
if (metadata.provider || metadata.model) {
|
|
1135
|
+
setMessages(prev => prev.map(msg => msg.id === assistantMessageId && isChatMessage(msg)
|
|
1136
|
+
? {
|
|
1137
|
+
...msg,
|
|
1138
|
+
metadata: {
|
|
1139
|
+
...msg.metadata,
|
|
1140
|
+
provider: metadata.provider,
|
|
1141
|
+
model: metadata.model
|
|
1142
|
+
}
|
|
1117
1143
|
}
|
|
1144
|
+
: msg));
|
|
1145
|
+
}
|
|
1146
|
+
},
|
|
1147
|
+
// onDebug callback - show debug status in gray
|
|
1148
|
+
(status) => {
|
|
1149
|
+
setDebugStatus(status);
|
|
1150
|
+
},
|
|
1151
|
+
// onInterrupt callback - show approval dialog
|
|
1152
|
+
(interrupt) => {
|
|
1153
|
+
approvalPendingRef.current = true;
|
|
1154
|
+
// Capture threadId from interrupt for context persistence
|
|
1155
|
+
if (interrupt.threadId && !agentThreadId) {
|
|
1156
|
+
setAgentThreadId(interrupt.threadId);
|
|
1157
|
+
console.log('[AgentPanel] Captured agentThreadId from interrupt:', interrupt.threadId);
|
|
1158
|
+
}
|
|
1159
|
+
// Auto-approve search/file/resource tools - execute immediately without user interaction
|
|
1160
|
+
if (interrupt.action === 'search_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;
|
|
1118
1182
|
}
|
|
1119
|
-
: msg));
|
|
1120
|
-
}
|
|
1121
|
-
},
|
|
1122
|
-
// onDebug callback - show debug status in gray
|
|
1123
|
-
(status) => {
|
|
1124
|
-
setDebugStatus(status);
|
|
1125
|
-
},
|
|
1126
|
-
// onInterrupt callback - show approval dialog
|
|
1127
|
-
(interrupt) => {
|
|
1128
|
-
approvalPendingRef.current = true;
|
|
1129
|
-
// Capture threadId from interrupt for context persistence
|
|
1130
|
-
if (interrupt.threadId && !agentThreadId) {
|
|
1131
|
-
setAgentThreadId(interrupt.threadId);
|
|
1132
|
-
console.log('[AgentPanel] Captured agentThreadId from interrupt:', interrupt.threadId);
|
|
1133
|
-
}
|
|
1134
|
-
// Auto-approve search/file/resource tools - execute immediately without user interaction
|
|
1135
|
-
if (interrupt.action === 'search_workspace_tool'
|
|
1136
|
-
|| interrupt.action === 'search_notebook_cells_tool'
|
|
1137
|
-
|| interrupt.action === 'check_resource_tool'
|
|
1138
|
-
|| interrupt.action === 'list_files_tool'
|
|
1139
|
-
|| interrupt.action === 'read_file_tool') {
|
|
1140
|
-
void handleAutoToolInterrupt(interrupt);
|
|
1141
|
-
return;
|
|
1142
|
-
}
|
|
1143
|
-
if (autoApproveEnabled) {
|
|
1144
|
-
void resumeFromInterrupt(interrupt, 'approve');
|
|
1145
|
-
return;
|
|
1146
|
-
}
|
|
1147
|
-
if (interrupt.action === 'jupyter_cell_tool' && interrupt.args?.code) {
|
|
1148
|
-
const shouldQueue = shouldExecuteInNotebook(interrupt.args.code);
|
|
1149
|
-
if (isAutoApprovedCode(interrupt.args.code)) {
|
|
1150
1183
|
if (shouldQueue) {
|
|
1151
1184
|
queueApprovalCell(interrupt.args.code);
|
|
1152
1185
|
}
|
|
1153
|
-
void resumeFromInterrupt(interrupt, 'approve');
|
|
1154
|
-
return;
|
|
1155
1186
|
}
|
|
1156
|
-
|
|
1157
|
-
|
|
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);
|
|
1158
1207
|
}
|
|
1159
|
-
}
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
setIsStreaming(false);
|
|
1164
|
-
},
|
|
1165
|
-
// onTodos callback - update todo list UI
|
|
1166
|
-
(newTodos) => {
|
|
1167
|
-
setTodos(newTodos);
|
|
1168
|
-
},
|
|
1169
|
-
// onDebugClear callback - clear debug status
|
|
1170
|
-
() => {
|
|
1171
|
-
setDebugStatus(null);
|
|
1172
|
-
},
|
|
1173
|
-
// onToolCall callback - add cells to notebook
|
|
1174
|
-
handleToolCall,
|
|
1175
|
-
// onComplete callback - capture thread_id for context persistence
|
|
1176
|
-
(data) => {
|
|
1177
|
-
if (data.threadId) {
|
|
1178
|
-
setAgentThreadId(data.threadId);
|
|
1179
|
-
console.log('[AgentPanel] Captured agentThreadId for context persistence:', data.threadId);
|
|
1180
|
-
}
|
|
1181
|
-
},
|
|
1182
|
-
// threadId - pass existing thread_id to continue context
|
|
1183
|
-
agentThreadId || undefined);
|
|
1208
|
+
},
|
|
1209
|
+
// threadId - pass existing thread_id to continue context
|
|
1210
|
+
agentThreadId || undefined);
|
|
1211
|
+
}
|
|
1184
1212
|
}
|
|
1185
1213
|
catch (error) {
|
|
1186
1214
|
const message = error instanceof Error ? error.message : 'Failed to send message';
|
|
@@ -2013,7 +2041,7 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
|
|
|
2013
2041
|
execution_method: 'subprocess'
|
|
2014
2042
|
};
|
|
2015
2043
|
};
|
|
2016
|
-
const upsertInterruptMessage = (interrupt) => {
|
|
2044
|
+
const upsertInterruptMessage = (interrupt, autoApproved) => {
|
|
2017
2045
|
const interruptMessageId = interruptMessageIdRef.current || makeMessageId('interrupt');
|
|
2018
2046
|
interruptMessageIdRef.current = interruptMessageId;
|
|
2019
2047
|
const interruptMessage = {
|
|
@@ -2021,7 +2049,14 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
|
|
|
2021
2049
|
role: 'system',
|
|
2022
2050
|
content: interrupt.description || '코드 실행 승인이 필요합니다.',
|
|
2023
2051
|
timestamp: Date.now(),
|
|
2024
|
-
metadata: {
|
|
2052
|
+
metadata: {
|
|
2053
|
+
interrupt: {
|
|
2054
|
+
...interrupt,
|
|
2055
|
+
resolved: autoApproved || false,
|
|
2056
|
+
decision: autoApproved ? 'approve' : undefined,
|
|
2057
|
+
autoApproved: autoApproved || false
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2025
2060
|
};
|
|
2026
2061
|
setMessages(prev => {
|
|
2027
2062
|
const hasExisting = prev.some(msg => msg.id === interruptMessageId);
|
|
@@ -2237,7 +2272,7 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
|
|
|
2237
2272
|
console.log('[AgentPanel] search_notebook_cells result:', executionResult);
|
|
2238
2273
|
}
|
|
2239
2274
|
else if (action === 'check_resource_tool') {
|
|
2240
|
-
const filesList =
|
|
2275
|
+
const filesList = args?.files || [];
|
|
2241
2276
|
setDebugStatus(`📊 리소스 체크 중: ${filesList.join(', ') || 'system'}`);
|
|
2242
2277
|
executionResult = await apiService.checkResource({
|
|
2243
2278
|
files: filesList,
|
|
@@ -2315,6 +2350,8 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
|
|
|
2315
2350
|
return;
|
|
2316
2351
|
}
|
|
2317
2352
|
if (autoApproveEnabled) {
|
|
2353
|
+
// 자동 승인 모드일 때도 인터럽트 메시지를 생성하여 코드블럭으로 표시
|
|
2354
|
+
upsertInterruptMessage(nextInterrupt, true);
|
|
2318
2355
|
void resumeFromInterrupt(nextInterrupt, 'approve');
|
|
2319
2356
|
return;
|
|
2320
2357
|
}
|
|
@@ -2390,6 +2427,276 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
|
|
|
2390
2427
|
};
|
|
2391
2428
|
}
|
|
2392
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
|
+
}
|
|
2393
2700
|
else if (interrupt.action === 'execute_command_tool') {
|
|
2394
2701
|
const command = (interrupt.args?.command || '').trim();
|
|
2395
2702
|
// Default stdin to "y\n" for interactive prompts (yes/no)
|
|
@@ -2541,6 +2848,8 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
|
|
|
2541
2848
|
return;
|
|
2542
2849
|
}
|
|
2543
2850
|
if (autoApproveEnabled) {
|
|
2851
|
+
// 자동 승인 모드일 때도 인터럽트 메시지를 생성하여 코드블럭으로 표시
|
|
2852
|
+
upsertInterruptMessage(nextInterrupt, true);
|
|
2544
2853
|
void resumeFromInterrupt(nextInterrupt, 'approve');
|
|
2545
2854
|
return;
|
|
2546
2855
|
}
|
|
@@ -2633,26 +2942,24 @@ const ChatPanel = (0,react__WEBPACK_IMPORTED_MODULE_0__.forwardRef)(({ apiServic
|
|
|
2633
2942
|
}
|
|
2634
2943
|
// 노트북이 없는 경우
|
|
2635
2944
|
if (!notebook) {
|
|
2636
|
-
// Python 에러가 감지되면 파일 수정 모드로 전환
|
|
2637
|
-
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
console.log('[AgentPanel] Agent mode: No file path found, continuing...');
|
|
2655
|
-
}
|
|
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
|
+
// }
|
|
2656
2963
|
// 파일 수정 관련 자연어 요청 감지 (에러, 고쳐, 수정, fix 등)
|
|
2657
2964
|
const fileFixRequestPatterns = [
|
|
2658
2965
|
/에러.*해결/i,
|
|
@@ -2745,29 +3052,25 @@ SyntaxError: '(' was never closed
|
|
|
2745
3052
|
return;
|
|
2746
3053
|
}
|
|
2747
3054
|
}
|
|
2748
|
-
// Python 에러 감지 및 파일 수정 모드 (Chat 모드에서만)
|
|
2749
|
-
//
|
|
2750
|
-
//
|
|
2751
|
-
if (inputMode === 'chat' && detectPythonError(currentInput)) {
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
// 파일 경로를 찾지 못해 handled=false 반환됨
|
|
2768
|
-
// 일반 스트림으로 폴백 (아래 로직에서 사용자 메시지 추가됨)
|
|
2769
|
-
console.log('[AgentPanel] No file path found in error, using regular LLM stream');
|
|
2770
|
-
}
|
|
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
|
+
// }
|
|
2771
3074
|
// Use the display prompt (input) for the user message, or use a fallback if input is empty
|
|
2772
3075
|
const displayContent = currentInput || (llmPrompt ? '셀 분석 요청' : '');
|
|
2773
3076
|
await sendChatMessage({
|
|
@@ -2803,26 +3106,26 @@ SyntaxError: '(' was never closed
|
|
|
2803
3106
|
e.preventDefault();
|
|
2804
3107
|
handleSendMessage();
|
|
2805
3108
|
}
|
|
2806
|
-
// Shift+Tab: 모드 전환 (chat
|
|
3109
|
+
// Shift+Tab: 모드 전환 (chat → agent → agent_v2 → chat 순환)
|
|
2807
3110
|
if (e.key === 'Tab' && e.shiftKey) {
|
|
2808
3111
|
e.preventDefault();
|
|
2809
|
-
setInputMode(prev => prev === 'chat' ? 'agent' : 'chat');
|
|
3112
|
+
setInputMode(prev => prev === 'chat' ? 'agent' : prev === 'agent' ? 'agent_v2' : 'chat');
|
|
2810
3113
|
return;
|
|
2811
3114
|
}
|
|
2812
3115
|
// Cmd/Ctrl + . : 모드 전환 (대체 단축키)
|
|
2813
3116
|
if (e.key === '.' && (e.metaKey || e.ctrlKey)) {
|
|
2814
3117
|
e.preventDefault();
|
|
2815
|
-
setInputMode(prev => prev === 'chat' ? 'agent' : 'chat');
|
|
3118
|
+
setInputMode(prev => prev === 'chat' ? 'agent' : prev === 'agent' ? 'agent_v2' : 'chat');
|
|
2816
3119
|
}
|
|
2817
3120
|
// Tab (without Shift): Agent 모드일 때 드롭다운 토글
|
|
2818
|
-
if (e.key === 'Tab' && !e.shiftKey && inputMode
|
|
3121
|
+
if (e.key === 'Tab' && !e.shiftKey && inputMode !== 'chat') {
|
|
2819
3122
|
e.preventDefault();
|
|
2820
3123
|
setShowModeDropdown(prev => !prev);
|
|
2821
3124
|
}
|
|
2822
3125
|
};
|
|
2823
|
-
// 모드 토글 함수
|
|
3126
|
+
// 모드 토글 함수 (chat → agent → agent_v2 → chat 순환)
|
|
2824
3127
|
const toggleMode = () => {
|
|
2825
|
-
setInputMode(prev => prev === 'chat' ? 'agent' : 'chat');
|
|
3128
|
+
setInputMode(prev => prev === 'chat' ? 'agent' : prev === 'agent' ? 'agent_v2' : 'chat');
|
|
2826
3129
|
setShowModeDropdown(false);
|
|
2827
3130
|
};
|
|
2828
3131
|
const clearChat = () => {
|
|
@@ -2965,19 +3268,25 @@ SyntaxError: '(' was never closed
|
|
|
2965
3268
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("p", null, "\uC548\uB155\uD558\uC138\uC694! HDSP Agent\uC785\uB2C8\uB2E4."),
|
|
2966
3269
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("p", { className: "jp-agent-empty-hint" }, inputMode === 'agent'
|
|
2967
3270
|
? '노트북 작업을 자연어로 요청하세요. 예: "데이터 시각화 해줘"'
|
|
2968
|
-
:
|
|
3271
|
+
: inputMode === 'agent_v2'
|
|
3272
|
+
? 'Deep Agent 모드입니다. HITL 승인을 통해 도구 실행을 제어할 수 있습니다.'
|
|
3273
|
+
: '메시지를 입력하거나 아래 버튼으로 Agent 모드를 선택하세요.'))) : (messages.map(msg => {
|
|
2969
3274
|
if (isChatMessage(msg)) {
|
|
2970
3275
|
// 일반 Chat 메시지
|
|
2971
3276
|
const isAssistant = msg.role === 'assistant';
|
|
2972
3277
|
const isShellOutput = msg.metadata?.kind === 'shell-output';
|
|
2973
3278
|
const interruptAction = msg.metadata?.interrupt?.action;
|
|
2974
3279
|
const isWriteFile = interruptAction === 'write_file_tool';
|
|
3280
|
+
const isEditFile = interruptAction === 'edit_file_tool';
|
|
2975
3281
|
const writePath = (isWriteFile
|
|
2976
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;
|
|
2977
3286
|
const headerRole = msg.role === 'user'
|
|
2978
3287
|
? '사용자'
|
|
2979
3288
|
: msg.role === 'system'
|
|
2980
|
-
? (isShellOutput ? 'shell 실행' : '승인 요청')
|
|
3289
|
+
? (isShellOutput ? 'shell 실행' : (autoApproved ? '자동 승인됨' : '승인 요청'))
|
|
2981
3290
|
: 'Agent';
|
|
2982
3291
|
return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { key: msg.id, className: isAssistant
|
|
2983
3292
|
? 'jp-agent-message jp-agent-message-assistant-inline'
|
|
@@ -2985,21 +3294,43 @@ SyntaxError: '(' was never closed
|
|
|
2985
3294
|
!isAssistant && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-message-header" },
|
|
2986
3295
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-message-role" }, headerRole),
|
|
2987
3296
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-message-time" }, new Date(msg.timestamp).toLocaleTimeString()))),
|
|
2988
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: `jp-agent-message-content${streamingMessageId === msg.id ? ' streaming' : ''}${isShellOutput ? ' jp-agent-message-content-shell' : ''}` }, msg.role === 'system' && msg.metadata?.interrupt ? (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className:
|
|
2989
|
-
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)),
|
|
2990
3299
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-interrupt-action" },
|
|
2991
3300
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-interrupt-action-args" }, (() => {
|
|
2992
3301
|
const command = msg.metadata?.interrupt?.args?.command;
|
|
2993
3302
|
const code = msg.metadata?.interrupt?.args?.code || msg.metadata?.interrupt?.args?.content || '';
|
|
2994
|
-
|
|
2995
|
-
|
|
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
|
+
}
|
|
2996
3323
|
const resolved = msg.metadata?.interrupt?.resolved;
|
|
2997
3324
|
const decision = msg.metadata?.interrupt?.decision;
|
|
2998
|
-
const
|
|
2999
|
-
|
|
3000
|
-
const
|
|
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
|
|
3001
3330
|
? `<div class="jp-agent-interrupt-actions ${resolvedClass}">${resolvedText}</div>`
|
|
3002
|
-
:
|
|
3331
|
+
: resolved && autoApproved
|
|
3332
|
+
? '' // 자동 승인일 때는 actionHtml 비움 (코드블럭 헤더에 배지 표시)
|
|
3333
|
+
: `
|
|
3003
3334
|
<div class="code-block-actions jp-agent-interrupt-actions">
|
|
3004
3335
|
<button class="jp-agent-interrupt-approve-btn" data-action="approve">승인</button>
|
|
3005
3336
|
<button class="jp-agent-interrupt-reject-btn" data-action="reject">거부</button>
|
|
@@ -3011,7 +3342,12 @@ SyntaxError: '(' was never closed
|
|
|
3011
3342
|
const safePath = escapeHtml(writePath);
|
|
3012
3343
|
html = html.replace(/<span class="code-block-language">[^<]*<\/span>/, `<span class="code-block-language jp-agent-interrupt-path">${safePath}</span>`);
|
|
3013
3344
|
}
|
|
3014
|
-
|
|
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;
|
|
3015
3351
|
})();
|
|
3016
3352
|
return (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-RenderedHTMLCommon", style: { padding: '0 4px' }, dangerouslySetInnerHTML: { __html: renderedHtml }, onClick: (event) => {
|
|
3017
3353
|
const target = event.target;
|
|
@@ -3073,7 +3409,7 @@ SyntaxError: '(' was never closed
|
|
|
3073
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"))))),
|
|
3074
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"))),
|
|
3075
3411
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { ref: messagesEndRef })),
|
|
3076
|
-
todos.length > 0 && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-todo-compact" },
|
|
3412
|
+
inputMode !== 'chat' && todos.length > 0 && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-todo-compact" },
|
|
3077
3413
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-todo-compact-header", onClick: () => setIsTodoExpanded(!isTodoExpanded) },
|
|
3078
3414
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-todo-compact-left" },
|
|
3079
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" },
|
|
@@ -3117,21 +3453,25 @@ SyntaxError: '(' was never closed
|
|
|
3117
3453
|
!statusText.startsWith('오류:') && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-debug-ellipsis", "aria-hidden": "true" }))))),
|
|
3118
3454
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-input-container" },
|
|
3119
3455
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-input-wrapper" },
|
|
3120
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("textarea", { className: `jp-agent-input ${inputMode
|
|
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
|
|
3121
3457
|
? '다른 방향 제시'
|
|
3122
3458
|
: (inputMode === 'agent'
|
|
3123
3459
|
? '노트북 작업을 입력하세요... (예: 데이터 시각화 해줘)'
|
|
3124
|
-
:
|
|
3460
|
+
: inputMode === 'agent_v2'
|
|
3461
|
+
? 'Deep Agent 요청을 입력하세요... (HITL 승인 모드)'
|
|
3462
|
+
: '메시지를 입력하세요...'), rows: 3, disabled: isLoading || isAgentRunning }),
|
|
3125
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 ? '거부' : '전송'))),
|
|
3126
3464
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-mode-bar" },
|
|
3127
3465
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-mode-toggle-container" },
|
|
3128
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("button", { className: `jp-agent-mode-toggle ${inputMode
|
|
3129
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("svg", { className: "jp-agent-mode-icon", viewBox: "0 0 16 16", fill: "currentColor", width: "14", height: "14" }, inputMode === '
|
|
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' ? (
|
|
3130
3470
|
// 무한대 아이콘 (Agent 모드)
|
|
3131
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" })) : (
|
|
3132
|
-
//
|
|
3133
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("path", { d: "M8
|
|
3134
|
-
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'),
|
|
3135
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" },
|
|
3136
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" }))),
|
|
3137
3477
|
showModeDropdown && (react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-mode-dropdown" },
|
|
@@ -3144,7 +3484,12 @@ SyntaxError: '(' was never closed
|
|
|
3144
3484
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("svg", { viewBox: "0 0 16 16", fill: "currentColor", width: "14", height: "14" },
|
|
3145
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" })),
|
|
3146
3486
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", null, "Agent"),
|
|
3147
|
-
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-mode-shortcut" }, "\uB178\uD2B8\uBD81 \uC790\uB3D9 \uC2E4\uD589"))
|
|
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)"))))),
|
|
3148
3493
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-mode-hints" },
|
|
3149
3494
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("span", { className: "jp-agent-mode-hint" }, "\u21E7Tab \uBAA8\uB4DC \uC804\uD658")))),
|
|
3150
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 }))));
|
|
@@ -3641,6 +3986,7 @@ const SettingsPanel = ({ onClose, onSave, currentConfig }) => {
|
|
|
3641
3986
|
const [systemPrompt, setSystemPrompt] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(initConfig.systemPrompt || '');
|
|
3642
3987
|
const [workspaceRoot, setWorkspaceRoot] = (0,react__WEBPACK_IMPORTED_MODULE_0__.useState)(initConfig.workspaceRoot || '');
|
|
3643
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);
|
|
3644
3990
|
// Update state when currentConfig changes
|
|
3645
3991
|
(0,react__WEBPACK_IMPORTED_MODULE_0__.useEffect)(() => {
|
|
3646
3992
|
if (currentConfig) {
|
|
@@ -3664,6 +4010,7 @@ const SettingsPanel = ({ onClose, onSave, currentConfig }) => {
|
|
|
3664
4010
|
setSystemPrompt(currentConfig.systemPrompt || (0,_services_ApiKeyManager__WEBPACK_IMPORTED_MODULE_1__.getDefaultLLMConfig)().systemPrompt || '');
|
|
3665
4011
|
setWorkspaceRoot(currentConfig.workspaceRoot || '');
|
|
3666
4012
|
setAutoApprove(Boolean(currentConfig.autoApprove));
|
|
4013
|
+
setIdleTimeoutMinutes(currentConfig.idleTimeoutMinutes ?? 60);
|
|
3667
4014
|
}
|
|
3668
4015
|
}, [currentConfig]);
|
|
3669
4016
|
// Helper: Build LLM config from state
|
|
@@ -3685,7 +4032,8 @@ const SettingsPanel = ({ onClose, onSave, currentConfig }) => {
|
|
|
3685
4032
|
},
|
|
3686
4033
|
workspaceRoot: workspaceRoot.trim() ? workspaceRoot.trim() : undefined,
|
|
3687
4034
|
systemPrompt: systemPrompt && systemPrompt.trim() ? systemPrompt : undefined,
|
|
3688
|
-
autoApprove
|
|
4035
|
+
autoApprove,
|
|
4036
|
+
idleTimeoutMinutes
|
|
3689
4037
|
});
|
|
3690
4038
|
// Handlers for multiple API keys
|
|
3691
4039
|
const handleAddKey = () => {
|
|
@@ -3853,6 +4201,11 @@ const SettingsPanel = ({ onClose, onSave, currentConfig }) => {
|
|
|
3853
4201
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", { className: "jp-agent-settings-checkbox" },
|
|
3854
4202
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("input", { type: "checkbox", checked: autoApprove, onChange: (e) => setAutoApprove(e.target.checked), "data-testid": "auto-approve-checkbox" }),
|
|
3855
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" })),
|
|
3856
4209
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("div", { className: "jp-agent-settings-group" },
|
|
3857
4210
|
react__WEBPACK_IMPORTED_MODULE_0___default().createElement("label", { className: "jp-agent-settings-label" },
|
|
3858
4211
|
"System Prompt (LangChain)",
|
|
@@ -4008,6 +4361,8 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
4008
4361
|
/* harmony import */ var _plugins_cell_buttons_plugin__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./plugins/cell-buttons-plugin */ "./lib/plugins/cell-buttons-plugin.js");
|
|
4009
4362
|
/* harmony import */ var _plugins_prompt_generation_plugin__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(/*! ./plugins/prompt-generation-plugin */ "./lib/plugins/prompt-generation-plugin.js");
|
|
4010
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");
|
|
4011
4366
|
/**
|
|
4012
4367
|
* Jupyter Agent Extension Entry Point
|
|
4013
4368
|
*/
|
|
@@ -4016,6 +4371,8 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
4016
4371
|
|
|
4017
4372
|
|
|
4018
4373
|
|
|
4374
|
+
|
|
4375
|
+
|
|
4019
4376
|
// Import styles
|
|
4020
4377
|
// import '../style/index.css';
|
|
4021
4378
|
/**
|
|
@@ -4027,7 +4384,9 @@ const plugins = [
|
|
|
4027
4384
|
_plugins_sidebar_plugin__WEBPACK_IMPORTED_MODULE_0__.sidebarPlugin,
|
|
4028
4385
|
_plugins_cell_buttons_plugin__WEBPACK_IMPORTED_MODULE_1__.cellButtonsPlugin,
|
|
4029
4386
|
_plugins_prompt_generation_plugin__WEBPACK_IMPORTED_MODULE_2__.promptGenerationPlugin,
|
|
4030
|
-
_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
|
|
4031
4390
|
];
|
|
4032
4391
|
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (plugins);
|
|
4033
4392
|
|
|
@@ -4827,60 +5186,1020 @@ function showCustomPromptDialog(cell) {
|
|
|
4827
5186
|
showNotification('질문 내용을 입력해주세요.', 'warning');
|
|
4828
5187
|
return;
|
|
4829
5188
|
}
|
|
4830
|
-
dialogOverlay.remove();
|
|
4831
|
-
// Get cell output
|
|
4832
|
-
const cellOutput = getCellOutput(cell);
|
|
4833
|
-
// Create display prompt: "Cell x: Custom request"
|
|
4834
|
-
const displayPrompt = `${cellIndex}번째 셀: ${promptText}`;
|
|
4835
|
-
// Create LLM prompt with code and output
|
|
4836
|
-
let llmPrompt = `${promptText}\n\n셀 내용:\n\`\`\`\n${cellContent}\n\`\`\``;
|
|
4837
|
-
if (cellOutput) {
|
|
4838
|
-
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;
|
|
4839
5963
|
}
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
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
|
+
});
|
|
4846
6012
|
}
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
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
|
+
}
|
|
4852
6030
|
}
|
|
4853
6031
|
}
|
|
4854
|
-
|
|
4855
|
-
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
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
|
+
};
|
|
4867
6060
|
}
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
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';
|
|
4873
6098
|
}
|
|
4874
|
-
}
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
6099
|
+
}
|
|
6100
|
+
/**
|
|
6101
|
+
* Convert file path to URI
|
|
6102
|
+
*/
|
|
6103
|
+
pathToUri(path) {
|
|
6104
|
+
if (path.startsWith('file://')) {
|
|
6105
|
+
return path;
|
|
4880
6106
|
}
|
|
4881
|
-
|
|
4882
|
-
|
|
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
|
|
4883
6201
|
}
|
|
6202
|
+
/* harmony default export */ const __WEBPACK_DEFAULT_EXPORT__ = (lspBridgePlugin);
|
|
4884
6203
|
|
|
4885
6204
|
|
|
4886
6205
|
/***/ },
|
|
@@ -7735,7 +9054,14 @@ const STORAGE_KEY = 'hdsp-agent-llm-config';
|
|
|
7735
9054
|
const DEFAULT_LANGCHAIN_SYSTEM_PROMPT = `You are an expert Python data scientist and Jupyter notebook assistant.
|
|
7736
9055
|
Your role is to help users with data analysis, visualization, and Python coding tasks in Jupyter notebooks. You can use only Korean
|
|
7737
9056
|
|
|
7738
|
-
|
|
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.
|
|
7739
9065
|
|
|
7740
9066
|
You MUST ALWAYS call a tool in every response. After any tool result, you MUST:
|
|
7741
9067
|
1. Check your todo list - are there pending or in_progress items?
|
|
@@ -7751,8 +9077,6 @@ You MUST ALWAYS call a tool in every response. After any tool result, you MUST:
|
|
|
7751
9077
|
}
|
|
7752
9078
|
4. If ALL todos are completed → call final_answer_tool with a summary
|
|
7753
9079
|
|
|
7754
|
-
NEVER end your turn without calling a tool. NEVER produce an empty response.
|
|
7755
|
-
|
|
7756
9080
|
## 🔴 MANDATORY: Resource Check Before Data Hanlding
|
|
7757
9081
|
**ALWAYS call check_resource_tool FIRST** when the task involves:
|
|
7758
9082
|
- Loading files: .csv, .parquet, .json, .xlsx, .pickle, .h5, .feather
|
|
@@ -7771,6 +9095,28 @@ NEVER end your turn without calling a tool. NEVER produce an empty response.
|
|
|
7771
9095
|
- Stopping after any tool without calling the next tool
|
|
7772
9096
|
- Ending without calling final_answer_tool
|
|
7773
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.
|
|
7774
9120
|
`;
|
|
7775
9121
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
7776
9122
|
// Key Rotation State (in-memory, not persisted)
|
|
@@ -7804,6 +9150,8 @@ function saveLLMConfig(config) {
|
|
|
7804
9150
|
try {
|
|
7805
9151
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
|
|
7806
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 }));
|
|
7807
9155
|
}
|
|
7808
9156
|
catch (e) {
|
|
7809
9157
|
console.error('[ApiKeyManager] Failed to save config:', e);
|
|
@@ -8262,7 +9610,102 @@ class ApiService {
|
|
|
8262
9610
|
* - Server receives ONLY ONE key per request
|
|
8263
9611
|
* - On 429 rate limit, frontend rotates key and retries with next key
|
|
8264
9612
|
*/
|
|
8265
|
-
|
|
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
|
+
}
|
|
9698
|
+
}
|
|
9699
|
+
}
|
|
9700
|
+
finally {
|
|
9701
|
+
reader.releaseLock();
|
|
9702
|
+
}
|
|
9703
|
+
}
|
|
9704
|
+
/**
|
|
9705
|
+
* Agent V2용 스트리밍 - /agent/langchain/stream 사용
|
|
9706
|
+
* LangChain Deep Agent (HITL, Todo, 도구 실행 등)
|
|
9707
|
+
*/
|
|
9708
|
+
async sendAgentV2Stream(request, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall, onComplete, // Callback to capture thread_id for context persistence
|
|
8266
9709
|
threadId // Optional thread_id to continue existing conversation
|
|
8267
9710
|
) {
|
|
8268
9711
|
// Maximum retry attempts (should match number of keys)
|
|
@@ -8275,7 +9718,7 @@ class ApiService {
|
|
|
8275
9718
|
? { ...request, llmConfig: (0,_ApiKeyManager__WEBPACK_IMPORTED_MODULE_0__.buildSingleKeyConfig)(currentConfig) }
|
|
8276
9719
|
: request;
|
|
8277
9720
|
try {
|
|
8278
|
-
await this.
|
|
9721
|
+
await this.sendAgentV2StreamInternal(requestToSend, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall, onComplete, threadId);
|
|
8279
9722
|
// Success - reset key rotation state
|
|
8280
9723
|
(0,_ApiKeyManager__WEBPACK_IMPORTED_MODULE_0__.resetKeyRotation)();
|
|
8281
9724
|
return;
|
|
@@ -8322,10 +9765,18 @@ class ApiService {
|
|
|
8322
9765
|
};
|
|
8323
9766
|
}
|
|
8324
9767
|
/**
|
|
8325
|
-
*
|
|
8326
|
-
*
|
|
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에서 이름 변경)
|
|
8327
9778
|
*/
|
|
8328
|
-
async
|
|
9779
|
+
async sendAgentV2StreamInternal(request, onChunk, onMetadata, onDebug, onInterrupt, onTodos, onDebugClear, onToolCall, onComplete, threadId) {
|
|
8329
9780
|
// Convert IChatRequest to LangChain AgentRequest format
|
|
8330
9781
|
// Frontend's context has limited fields, map what's available
|
|
8331
9782
|
const resourceContext = await this.getResourceUsageSnapshot();
|
|
@@ -8713,6 +10164,24 @@ class ApiService {
|
|
|
8713
10164
|
}
|
|
8714
10165
|
return result;
|
|
8715
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
|
+
}
|
|
8716
10185
|
async writeFile(path, content, options) {
|
|
8717
10186
|
const response = await fetch(`${this.baseUrl}/write-file`, {
|
|
8718
10187
|
method: 'POST',
|
|
@@ -8799,6 +10268,14 @@ class ApiService {
|
|
|
8799
10268
|
});
|
|
8800
10269
|
const payload = await response.json().catch(() => ({}));
|
|
8801
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
|
+
}
|
|
8802
10279
|
const errorMessage = payload.error || 'Failed to check resources';
|
|
8803
10280
|
throw new Error(errorMessage);
|
|
8804
10281
|
}
|
|
@@ -13513,9 +14990,11 @@ class SafetyChecker {
|
|
|
13513
14990
|
|
|
13514
14991
|
__webpack_require__.r(__webpack_exports__);
|
|
13515
14992
|
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
14993
|
+
/* harmony export */ countDiffLines: () => (/* binding */ countDiffLines),
|
|
13516
14994
|
/* harmony export */ escapeHtml: () => (/* binding */ escapeHtml),
|
|
13517
14995
|
/* harmony export */ formatInlineMarkdown: () => (/* binding */ formatInlineMarkdown),
|
|
13518
14996
|
/* harmony export */ formatMarkdownToHtml: () => (/* binding */ formatMarkdownToHtml),
|
|
14997
|
+
/* harmony export */ highlightDiff: () => (/* binding */ highlightDiff),
|
|
13519
14998
|
/* harmony export */ highlightJavaScript: () => (/* binding */ highlightJavaScript),
|
|
13520
14999
|
/* harmony export */ highlightPython: () => (/* binding */ highlightPython),
|
|
13521
15000
|
/* harmony export */ normalizeIndentation: () => (/* binding */ normalizeIndentation),
|
|
@@ -13849,6 +15328,52 @@ function highlightPython(code) {
|
|
|
13849
15328
|
});
|
|
13850
15329
|
return highlighted;
|
|
13851
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
|
+
}
|
|
13852
15377
|
/**
|
|
13853
15378
|
* Highlight JavaScript code
|
|
13854
15379
|
*/
|
|
@@ -14155,16 +15680,29 @@ function formatMarkdownToHtml(text) {
|
|
|
14155
15680
|
language: lang
|
|
14156
15681
|
});
|
|
14157
15682
|
// Create HTML for code block
|
|
14158
|
-
const
|
|
14159
|
-
|
|
14160
|
-
|
|
14161
|
-
|
|
14162
|
-
|
|
14163
|
-
|
|
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 + '">' +
|
|
14164
15701
|
'<div class="code-block-header">' +
|
|
14165
15702
|
'<span class="code-block-language">' + escapeHtml(lang) + '</span>' +
|
|
15703
|
+
diffLineCountHtml +
|
|
14166
15704
|
'<div class="code-block-actions">' +
|
|
14167
|
-
'<button class="code-block-apply" data-block-id="' + blockId + '" title="셀에 적용">셀에 적용</button>' +
|
|
15705
|
+
(isDiff ? '' : '<button class="code-block-apply" data-block-id="' + blockId + '" title="셀에 적용">셀에 적용</button>') +
|
|
14168
15706
|
'<button class="code-block-copy" data-block-id="' + blockId + '" title="복사">복사</button>' +
|
|
14169
15707
|
'</div>' +
|
|
14170
15708
|
'</div>' +
|
|
@@ -14446,4 +15984,4 @@ __webpack_require__.r(__webpack_exports__);
|
|
|
14446
15984
|
/***/ }
|
|
14447
15985
|
|
|
14448
15986
|
}]);
|
|
14449
|
-
//# sourceMappingURL=lib_index_js.
|
|
15987
|
+
//# sourceMappingURL=lib_index_js.e4ff4b5779b5e049f84c.js.map
|