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