thonny-codemate 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (27) hide show
  1. thonny_codemate-0.1.0.dist-info/METADATA +307 -0
  2. thonny_codemate-0.1.0.dist-info/RECORD +27 -0
  3. thonny_codemate-0.1.0.dist-info/WHEEL +5 -0
  4. thonny_codemate-0.1.0.dist-info/licenses/LICENSE +21 -0
  5. thonny_codemate-0.1.0.dist-info/top_level.txt +1 -0
  6. thonnycontrib/__init__.py +1 -0
  7. thonnycontrib/thonny_codemate/__init__.py +397 -0
  8. thonnycontrib/thonny_codemate/api.py +154 -0
  9. thonnycontrib/thonny_codemate/context_manager.py +296 -0
  10. thonnycontrib/thonny_codemate/external_providers.py +714 -0
  11. thonnycontrib/thonny_codemate/i18n.py +506 -0
  12. thonnycontrib/thonny_codemate/llm_client.py +841 -0
  13. thonnycontrib/thonny_codemate/message_virtualization.py +136 -0
  14. thonnycontrib/thonny_codemate/model_manager.py +515 -0
  15. thonnycontrib/thonny_codemate/performance_monitor.py +141 -0
  16. thonnycontrib/thonny_codemate/prompts.py +102 -0
  17. thonnycontrib/thonny_codemate/ui/__init__.py +1 -0
  18. thonnycontrib/thonny_codemate/ui/chat_view.py +687 -0
  19. thonnycontrib/thonny_codemate/ui/chat_view_html.py +1299 -0
  20. thonnycontrib/thonny_codemate/ui/custom_prompt_dialog.py +175 -0
  21. thonnycontrib/thonny_codemate/ui/markdown_renderer.py +484 -0
  22. thonnycontrib/thonny_codemate/ui/model_download_dialog.py +355 -0
  23. thonnycontrib/thonny_codemate/ui/settings_dialog.py +1218 -0
  24. thonnycontrib/thonny_codemate/utils/__init__.py +25 -0
  25. thonnycontrib/thonny_codemate/utils/constants.py +138 -0
  26. thonnycontrib/thonny_codemate/utils/error_messages.py +92 -0
  27. thonnycontrib/thonny_codemate/utils/unified_error_handler.py +310 -0
@@ -0,0 +1,687 @@
1
+ """
2
+ LLMチャットビュー
3
+ GitHub Copilot風の右側パネルUIを提供
4
+ """
5
+ import tkinter as tk
6
+ from tkinter import ttk, scrolledtext, messagebox
7
+ import threading
8
+ import queue
9
+ import logging
10
+ import json
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+ from thonny import get_workbench
15
+
16
+ # 安全なロガーを使用
17
+ try:
18
+ from .. import get_safe_logger
19
+ logger = get_safe_logger(__name__)
20
+ except ImportError:
21
+ # フォールバック
22
+ logger = logging.getLogger(__name__)
23
+ logger.addHandler(logging.NullHandler())
24
+
25
+
26
+ class LLMChatView(ttk.Frame):
27
+ """
28
+ LLMとのチャットインターフェースを提供するビュー
29
+ Thonnyの右側に表示される
30
+ """
31
+
32
+ def __init__(self, master):
33
+ super().__init__(master)
34
+
35
+ self.llm_client = None
36
+ self._init_ui()
37
+ self._init_llm()
38
+
39
+ # メッセージキュー(スレッド間通信用)
40
+ self.message_queue = queue.Queue()
41
+ self._processing = False
42
+ self._first_token = True # ストリーミング用のフラグ
43
+ self._stop_generation = False # 生成を停止するフラグ
44
+
45
+ # 定期的にキューをチェック
46
+ self._queue_check_id = self.after(100, self._process_queue)
47
+
48
+ # ウィンドウ閉じるイベントをバインド
49
+ self.bind("<Destroy>", self._on_destroy)
50
+
51
+ # チャット履歴を格納
52
+ self.chat_history = []
53
+
54
+ # 会話履歴を読み込む
55
+ self._load_chat_history()
56
+
57
+ def _init_ui(self):
58
+ """UIコンポーネントを初期化"""
59
+ # メインコンテナ
60
+ self.columnconfigure(0, weight=1)
61
+ self.rowconfigure(1, weight=1)
62
+
63
+ # ヘッダー
64
+ header_frame = ttk.Frame(self)
65
+ header_frame.grid(row=0, column=0, sticky="ew", padx=5, pady=5)
66
+
67
+ ttk.Label(header_frame, text="LLM Assistant", font=("", 12, "bold")).pack(side=tk.LEFT)
68
+
69
+ # Clearボタン
70
+ self.clear_button = ttk.Button(
71
+ header_frame,
72
+ text="Clear",
73
+ command=self._clear_chat,
74
+ width=8
75
+ )
76
+ self.clear_button.pack(side=tk.LEFT, padx=10)
77
+
78
+ # 設定ボタン
79
+ self.settings_button = ttk.Button(
80
+ header_frame,
81
+ text="⚙",
82
+ width=3,
83
+ command=self._show_settings
84
+ )
85
+ self.settings_button.pack(side=tk.RIGHT, padx=2)
86
+
87
+ # ステータスフレーム(モデル名とステータス)
88
+ status_frame = ttk.Frame(header_frame)
89
+ status_frame.pack(side=tk.RIGHT, padx=5)
90
+
91
+ self.status_label = ttk.Label(status_frame, text="Not loaded", foreground="gray")
92
+ self.status_label.pack(side=tk.RIGHT)
93
+
94
+ # チャット表示エリア
95
+ chat_frame = ttk.Frame(self)
96
+ chat_frame.grid(row=1, column=0, sticky="nsew", padx=5, pady=5)
97
+ chat_frame.columnconfigure(0, weight=1)
98
+ chat_frame.rowconfigure(0, weight=1)
99
+
100
+ self.chat_display = scrolledtext.ScrolledText(
101
+ chat_frame,
102
+ wrap=tk.WORD,
103
+ width=40,
104
+ height=20,
105
+ font=("Consolas", 10),
106
+ state=tk.DISABLED,
107
+ background="#f8f8f8"
108
+ )
109
+ self.chat_display.grid(row=0, column=0, sticky="nsew")
110
+
111
+ # タグの設定
112
+ self.chat_display.tag_config("user", foreground="#0066cc", font=("Consolas", 10, "bold"))
113
+ self.chat_display.tag_config("assistant", foreground="#006600")
114
+ self.chat_display.tag_config("error", foreground="#cc0000")
115
+ self.chat_display.tag_config("code", background="#e8e8e8", font=("Consolas", 9))
116
+
117
+ # 入力エリア
118
+ input_frame = ttk.Frame(self)
119
+ input_frame.grid(row=2, column=0, sticky="ew", padx=5, pady=5)
120
+ input_frame.columnconfigure(0, weight=1)
121
+
122
+ # 入力テキスト
123
+ self.input_text = tk.Text(
124
+ input_frame,
125
+ height=3,
126
+ font=("Consolas", 10),
127
+ wrap=tk.WORD
128
+ )
129
+ self.input_text.grid(row=0, column=0, sticky="ew", pady=(0, 5))
130
+
131
+ # ボタンフレーム
132
+ button_frame = ttk.Frame(input_frame)
133
+ button_frame.grid(row=1, column=0, sticky="ew")
134
+
135
+ # send button
136
+ self.send_button = ttk.Button(
137
+ button_frame,
138
+ text="Send",
139
+ command=self._handle_send_button,
140
+ state=tk.DISABLED
141
+ )
142
+ self.send_button.pack(side=tk.RIGHT, padx=2)
143
+
144
+ # Ctrl+Enterのヒントラベル
145
+ hint_label = ttk.Label(
146
+ button_frame,
147
+ text="Ctrl+Enter to send",
148
+ foreground="gray",
149
+ font=("", 9)
150
+ )
151
+ hint_label.pack(side=tk.RIGHT, padx=5)
152
+
153
+
154
+ # プリセットボタン(幅を指定して文字が切れないようにする)
155
+ ttk.Button(
156
+ button_frame,
157
+ text="Explain Error",
158
+ command=self._explain_last_error,
159
+ width=15 # 幅を指定
160
+ ).pack(side=tk.LEFT, padx=2)
161
+
162
+
163
+ # コンテキストボタン
164
+ self.context_var = tk.BooleanVar(value=False)
165
+ self.context_check = ttk.Checkbutton(
166
+ button_frame,
167
+ text="Include Context",
168
+ variable=self.context_var,
169
+ command=self._toggle_context
170
+ )
171
+ self.context_check.pack(side=tk.LEFT, padx=10)
172
+
173
+ # コンテキストマネージャー
174
+ self.context_manager = None
175
+
176
+ # キーバインディング
177
+ self.input_text.bind("<Control-Return>", lambda e: self._handle_send_button())
178
+ self.input_text.bind("<Shift-Return>", lambda e: "break") # 改行を許可
179
+
180
+ # Escapeキーで生成を停止
181
+ self.bind_all("<Escape>", lambda e: self._stop_if_processing())
182
+
183
+ def _init_llm(self):
184
+ """LLMクライアントを初期化"""
185
+ try:
186
+ from .. import get_llm_client
187
+ from ..model_manager import ModelManager
188
+
189
+ self.llm_client = get_llm_client()
190
+
191
+ workbench = get_workbench()
192
+ provider = workbench.get_option("llm.provider", "local")
193
+
194
+ if provider == "local":
195
+ # ローカルモデルの場合
196
+ model_path = workbench.get_option("llm.model_path", "")
197
+
198
+ if not model_path or not Path(model_path).exists():
199
+ # モデルがない場合、利用可能なモデルをチェック
200
+ manager = ModelManager()
201
+ available_model = manager.get_model_path("llama3.2-1b") or manager.get_model_path("llama3.2-3b")
202
+
203
+ if available_model:
204
+ # 利用可能なモデルがある場合は自動設定
205
+ workbench.set_option("llm.model_path", available_model)
206
+ model_path = available_model
207
+ else:
208
+ # モデルがない場合はダウンロードを促す
209
+ self.status_label.config(text="No model loaded", foreground="red")
210
+ self._append_message(
211
+ "System",
212
+ "No model found. Please download a model from Settings → Download Models.",
213
+ "error"
214
+ )
215
+
216
+ # 設定ダイアログを開くボタンを表示
217
+ # 親ウィンドウを指定してダイアログを表示
218
+ if messagebox.askyesno(
219
+ "No Model Found",
220
+ "No LLM model found. Would you like to download recommended models?",
221
+ parent=self
222
+ ):
223
+ self._show_settings()
224
+ return
225
+
226
+ # 非同期でモデルをロード
227
+ # モデル名を表示しながらロード
228
+ model_name = Path(model_path).name if model_path else "model"
229
+ self.status_label.config(text=f"Loading {model_name}...", foreground="orange")
230
+ self.llm_client.load_model_async(callback=self._on_model_loaded)
231
+ else:
232
+ # 外部プロバイダーの場合、モデル名も含める
233
+ external_model = workbench.get_option("llm.external_model", "")
234
+ display_text = f"{external_model} ({provider})" if external_model else f"Using {provider}"
235
+ self.status_label.config(text=display_text, foreground="blue")
236
+ # config取得で外部プロバイダーが設定される
237
+ self.llm_client.get_config()
238
+ self.send_button.config(state=tk.NORMAL)
239
+ self._append_message(
240
+ "System",
241
+ f"Connected to {provider.upper()} API. Ready to chat!",
242
+ "assistant"
243
+ )
244
+
245
+ except Exception as e:
246
+ logger.error(f"Failed to initialize LLM client: {e}")
247
+ self.status_label.config(text="Error loading model", foreground="red")
248
+ self._append_message("System", f"Failed to initialize LLM: {str(e)}", "error")
249
+
250
+ def _on_model_loaded(self, success: bool, error: Optional[Exception]):
251
+ """モデル読み込み完了時のコールバック"""
252
+ def update_ui():
253
+ if success:
254
+ # モデル名を取得
255
+ model_path = get_workbench().get_option("llm.model_path", "")
256
+ model_name = Path(model_path).name if model_path else "Unknown"
257
+ self.status_label.config(text=f"{model_name} | Ready", foreground="green")
258
+ self.send_button.config(state=tk.NORMAL)
259
+ self._append_message("System", "LLM model loaded successfully!", "assistant")
260
+ else:
261
+ self.status_label.config(text="Load failed", foreground="red")
262
+ self._append_message("System", f"Failed to load model: {error}", "error")
263
+
264
+ # UIスレッドで更新
265
+ self.after(0, update_ui)
266
+
267
+ def _append_message(self, sender: str, message: str, tag: str = None):
268
+ """チャット表示にメッセージを追加"""
269
+ self.chat_display.config(state=tk.NORMAL)
270
+
271
+ # 送信者を表示
272
+ self.chat_display.insert(tk.END, f"\n{sender}: ", tag or "user")
273
+
274
+ # メッセージを表示
275
+ if tag == "code" or "```" in message:
276
+ # コードブロックを検出して整形
277
+ parts = message.split("```")
278
+ for i, part in enumerate(parts):
279
+ if i % 2 == 0:
280
+ self.chat_display.insert(tk.END, part, tag or "assistant")
281
+ else:
282
+ # コードブロック
283
+ lines = part.split('\n')
284
+ if lines[0]: # 言語指定がある場合
285
+ self.chat_display.insert(tk.END, f"\n[{lines[0]}]\n", "assistant")
286
+ code = '\n'.join(lines[1:])
287
+ else:
288
+ code = part
289
+ self.chat_display.insert(tk.END, code, "code")
290
+ else:
291
+ self.chat_display.insert(tk.END, message, tag or "assistant")
292
+
293
+ self.chat_display.insert(tk.END, "\n")
294
+ self.chat_display.see(tk.END)
295
+ self.chat_display.config(state=tk.DISABLED)
296
+
297
+ # 履歴に追加(システムメッセージ以外)
298
+ if sender not in ["System"] and not message.startswith("["):
299
+ self.chat_history.append({"sender": sender, "message": message})
300
+ # ユーザーとアシスタントのメッセージのみ保存
301
+ if sender in ["User", "Assistant"]:
302
+ self._save_chat_history()
303
+
304
+ def _handle_send_button(self):
305
+ """送信/停止ボタンのハンドラー"""
306
+ if self._processing:
307
+ # 生成中の場合は停止
308
+ self._stop_generation = True
309
+ self.send_button.config(text="Stopping...")
310
+ self._append_message("System", "Stopping generation...", "info")
311
+ else:
312
+ # 通常の送信処理
313
+ self._send_message()
314
+
315
+ def _stop_if_processing(self):
316
+ """処理中の場合は生成を停止"""
317
+ if self._processing:
318
+ self._stop_generation = True
319
+ self.send_button.config(text="Stopping...")
320
+ self._append_message("System", "Stopping generation... (ESC pressed)", "info")
321
+
322
+ def _send_message(self):
323
+ """メッセージを送信"""
324
+ message = self.input_text.get("1.0", tk.END).strip()
325
+ if not message:
326
+ return
327
+
328
+ # UIをクリア
329
+ self.input_text.delete("1.0", tk.END)
330
+
331
+ # コンテキスト情報を取得
332
+ context_info = None
333
+ if self.context_var.get() and self.context_manager:
334
+ workbench = get_workbench()
335
+ editor = workbench.get_editor_notebook().get_current_editor()
336
+ if editor:
337
+ current_file = editor.get_filename()
338
+ text_widget = editor.get_text_widget()
339
+
340
+ if text_widget.tag_ranges("sel"):
341
+ # 選択範囲がある場合
342
+ start_line = int(text_widget.index("sel.first").split(".")[0])
343
+ end_line = int(text_widget.index("sel.last").split(".")[0])
344
+ file_name = Path(current_file).name if current_file else "Unknown"
345
+ context_info = f"[Context: {file_name} (lines {start_line}-{end_line})]"
346
+ elif current_file:
347
+ # ファイル全体の場合
348
+ file_name = Path(current_file).name
349
+ context_info = f"[Context: {file_name} (entire file)]"
350
+
351
+ # ユーザーメッセージを追加(コンテキスト情報付き)
352
+ if context_info:
353
+ display_message = f"{message}\n\n{context_info}"
354
+ else:
355
+ display_message = message
356
+
357
+ self._append_message("You", display_message, "user")
358
+
359
+ # 処理中フラグ
360
+ self._processing = True
361
+ self._first_token = True # ストリーミング用フラグをリセット
362
+ self._stop_generation = False # 停止フラグをリセット
363
+ self.send_button.config(text="Stop", state=tk.NORMAL) # ボタンを停止モードに変更
364
+
365
+ # バックグラウンドで処理
366
+ thread = threading.Thread(
367
+ target=self._generate_response,
368
+ args=(message,), # 元のメッセージを渡す(コンテキスト情報なし)
369
+ daemon=True
370
+ )
371
+ thread.start()
372
+
373
+ def _generate_response(self, message: str):
374
+ """バックグラウンドで応答を生成"""
375
+ try:
376
+ # コンテキストを含める場合
377
+ if self.context_var.get() and self.context_manager:
378
+ # 現在のエディタのファイルパスを取得
379
+ workbench = get_workbench()
380
+ editor = workbench.get_editor_notebook().get_current_editor()
381
+ current_file = None
382
+ selected_text = None
383
+ selection_info = None
384
+
385
+ if editor:
386
+ current_file = editor.get_filename()
387
+ text_widget = editor.get_text_widget()
388
+
389
+ # 選択範囲があるかチェック
390
+ if text_widget.tag_ranges("sel"):
391
+ selected_text = text_widget.get("sel.first", "sel.last")
392
+ # 選択範囲の行番号を取得
393
+ start_line = int(text_widget.index("sel.first").split(".")[0])
394
+ end_line = int(text_widget.index("sel.last").split(".")[0])
395
+ selection_info = f"Selected lines: {start_line}-{end_line}"
396
+
397
+ if selected_text:
398
+ # 選択範囲がある場合はそれをコンテキストとして使用
399
+ # ファイル拡張子から言語を判定
400
+ file_ext = Path(current_file).suffix.lower() if current_file else '.py'
401
+ lang_map = {'.py': 'python', '.js': 'javascript', '.java': 'java', '.cpp': 'cpp', '.c': 'c'}
402
+ lang = lang_map.get(file_ext, 'python')
403
+
404
+ context_str = f"""File: {Path(current_file).name if current_file else 'Unknown'}
405
+ {selection_info}
406
+
407
+ ```{lang}
408
+ {selected_text}
409
+ ```"""
410
+ # コンテキスト使用メッセージは不要
411
+ elif editor and current_file:
412
+ # 選択範囲がない場合は、ファイル全体を取得
413
+ full_text = text_widget.get("1.0", tk.END).strip()
414
+ if full_text:
415
+ # ファイル拡張子から言語を判定
416
+ file_ext = Path(current_file).suffix.lower()
417
+ lang_map = {'.py': 'python', '.js': 'javascript', '.java': 'java', '.cpp': 'cpp', '.c': 'c'}
418
+ lang = lang_map.get(file_ext, 'python')
419
+
420
+ context_str = f"""File: {Path(current_file).name}
421
+ Full file content:
422
+
423
+ ```{lang}
424
+ {full_text}
425
+ ```"""
426
+ # コンテキスト使用メッセージは不要
427
+ else:
428
+ context_str = None
429
+ else:
430
+ context_str = None
431
+
432
+ if context_str:
433
+ # コンテキスト付きで生成
434
+ full_prompt = f"""Here is the context from the current project:
435
+
436
+ {context_str}
437
+
438
+ Based on this context, {message}"""
439
+
440
+ for token in self.llm_client.generate_stream(full_prompt):
441
+ if self._stop_generation:
442
+ self.message_queue.put(("info", "\n[Generation stopped by user]"))
443
+ break
444
+ self.message_queue.put(("token", token))
445
+ else:
446
+ # 通常の生成
447
+ for token in self.llm_client.generate_stream(message):
448
+ if self._stop_generation:
449
+ self.message_queue.put(("info", "\n[Generation stopped by user]"))
450
+ break
451
+ self.message_queue.put(("token", token))
452
+ else:
453
+ # 通常の生成
454
+ for token in self.llm_client.generate_stream(message):
455
+ if self._stop_generation:
456
+ self.message_queue.put(("info", "\n[Generation stopped by user]"))
457
+ break
458
+ self.message_queue.put(("token", token))
459
+
460
+ # 完了
461
+ self.message_queue.put(("complete", None))
462
+
463
+ except Exception as e:
464
+ logger.error(f"Error generating response: {e}")
465
+ self.message_queue.put(("error", str(e)))
466
+
467
+ def _process_queue(self):
468
+ """メッセージキューを処理"""
469
+ try:
470
+ # キューから全てのメッセージを処理
471
+ while True:
472
+ msg_type, content = self.message_queue.get_nowait()
473
+
474
+ if msg_type == "token":
475
+ if self._first_token:
476
+ # 最初のトークンの時だけAssistantラベルを追加
477
+ self.chat_display.config(state=tk.NORMAL)
478
+ self.chat_display.insert(tk.END, "\nAssistant: ", "role")
479
+ self.chat_display.config(state=tk.DISABLED)
480
+ self._first_token = False
481
+
482
+ # トークンを追加(ラベルなしで)
483
+ self.chat_display.config(state=tk.NORMAL)
484
+ self.chat_display.insert(tk.END, content, "assistant")
485
+ self.chat_display.see(tk.END)
486
+ self.chat_display.config(state=tk.DISABLED)
487
+
488
+ elif msg_type == "complete":
489
+ self._processing = False
490
+ self._stop_generation = False # 停止フラグをリセット
491
+ self.send_button.config(text="Send", state=tk.NORMAL) # ボタンを送信モードに戻す
492
+ self._first_token = True # 次のメッセージ用にリセット
493
+
494
+ elif msg_type == "error":
495
+ self._append_message("System", f"Error: {content}", "error")
496
+ self._processing = False
497
+ self._stop_generation = False # 停止フラグをリセット
498
+ self.send_button.config(text="Send", state=tk.NORMAL) # ボタンを送信モードに戻す
499
+ self._first_token = True # 次のメッセージ用にリセット
500
+
501
+ elif msg_type == "info":
502
+ self._append_message("System", content, "assistant")
503
+ self._first_token = True # 次のメッセージ用にリセット
504
+
505
+ except queue.Empty:
506
+ pass
507
+
508
+ # 次のチェックをスケジュール
509
+ self.after(50, self._process_queue)
510
+
511
+ def explain_code(self, code: str):
512
+ """コードを説明(外部から呼ばれる)"""
513
+ # 現在のスキルレベルを取得
514
+ workbench = get_workbench()
515
+ skill_level = workbench.get_option("llm.skill_level", "beginner")
516
+
517
+ # 現在のファイルから言語を検出
518
+ editor = workbench.get_editor_notebook().get_current_editor()
519
+ lang = 'python' # デフォルト
520
+ if editor:
521
+ filename = editor.get_filename()
522
+ if filename:
523
+ file_ext = Path(filename).suffix.lower()
524
+ lang_map = {'.py': 'python', '.js': 'javascript', '.java': 'java', '.cpp': 'cpp', '.c': 'c'}
525
+ lang = lang_map.get(file_ext, 'python')
526
+
527
+ # メッセージを作成
528
+ message = f"Please explain this code:\n```{lang}\n{code}\n```"
529
+
530
+ # 入力欄に設定して送信
531
+ self.input_text.delete("1.0", tk.END)
532
+ self.input_text.insert("1.0", message)
533
+ self._send_message()
534
+
535
+ def _explain_last_error(self):
536
+ """最後のエラーを説明"""
537
+ try:
538
+ # シェルビューを取得
539
+ shell_view = get_workbench().get_view("ShellView")
540
+ if not shell_view:
541
+ messagebox.showinfo("No Shell", "Shell view not found.")
542
+ return
543
+
544
+ # シェルのテキストウィジェットを取得
545
+ shell_text = shell_view.text
546
+
547
+ # シェルの内容を取得(最後の部分)
548
+ shell_content = shell_text.get("1.0", tk.END)
549
+ lines = shell_content.strip().split('\n')
550
+
551
+ # エラーを探す(後ろから検索)
552
+ error_lines = []
553
+ error_found = False
554
+
555
+ for i in range(len(lines) - 1, -1, -1):
556
+ line = lines[i]
557
+
558
+ # エラーの終わりを検出
559
+ if error_found and (line.startswith(">>>") or line.startswith("===") or not line.strip()):
560
+ break
561
+
562
+ # エラーパターンを検出
563
+ if any(error_type in line for error_type in ["Error", "Exception", "Traceback"]):
564
+ error_found = True
565
+
566
+ if error_found:
567
+ error_lines.insert(0, line)
568
+
569
+ if not error_lines:
570
+ messagebox.showinfo("No Error", "No recent error found in shell.")
571
+ return
572
+
573
+ # エラーメッセージを結合
574
+ error_message = '\n'.join(error_lines)
575
+
576
+ # 関連するコードも取得(エディタから)
577
+ code = ""
578
+ editor = get_workbench().get_editor_notebook().get_current_editor()
579
+ if editor:
580
+ try:
581
+ code = editor.get_text_widget().get("1.0", tk.END).strip()
582
+ except:
583
+ pass
584
+
585
+ # プロンプトを作成
586
+ if code:
587
+ prompt = f"I encountered this error:\n```\n{error_message}\n```\n\nIn this code:\n```python\n{code}\n```\n\nPlease explain what causes this error and how to fix it."
588
+ else:
589
+ prompt = f"I encountered this error:\n```\n{error_message}\n```\n\nPlease explain what causes this error and provide a solution."
590
+
591
+ # 入力欄に設定して送信
592
+ self.input_text.delete("1.0", tk.END)
593
+ self.input_text.insert("1.0", prompt)
594
+ self._send_message()
595
+
596
+ except Exception as e:
597
+ logger.error(f"Error in _explain_last_error: {e}")
598
+ messagebox.showerror("Error", f"Failed to get error information: {str(e)}")
599
+
600
+ def _show_settings(self):
601
+ """設定ダイアログを表示"""
602
+ from .settings_dialog import SettingsDialog
603
+ dialog = SettingsDialog(self)
604
+ dialog.grab_set()
605
+ self.wait_window(dialog)
606
+
607
+ # 設定が変更された可能性があるので再初期化
608
+ if hasattr(dialog, 'settings_changed') and dialog.settings_changed:
609
+ self._init_llm()
610
+
611
+ def _clear_chat(self):
612
+ """チャットをクリア"""
613
+ self.chat_display.config(state=tk.NORMAL)
614
+ self.chat_display.delete("1.0", tk.END)
615
+ self.chat_display.config(state=tk.DISABLED)
616
+
617
+ # 履歴もクリア
618
+ self.chat_history.clear()
619
+ self._save_chat_history()
620
+
621
+ def _toggle_context(self):
622
+ """コンテキストの有効/無効を切り替え"""
623
+ if self.context_var.get():
624
+ # コンテキストマネージャーを初期化
625
+ if not self.context_manager:
626
+ from ..context_manager import ContextManager
627
+ self.context_manager = ContextManager()
628
+
629
+ def _get_chat_history_path(self) -> Path:
630
+ """チャット履歴ファイルのパスを取得"""
631
+ workbench = get_workbench()
632
+ data_dir = Path(workbench.get_configuration_directory())
633
+ llm_dir = data_dir / "llm_assistant"
634
+ llm_dir.mkdir(exist_ok=True)
635
+ return llm_dir / "chat_history_text.json"
636
+
637
+ def _save_chat_history(self):
638
+ """チャット履歴を保存"""
639
+ try:
640
+ # 最新の100メッセージのみ保存
641
+ messages_to_save = self.chat_history[-100:]
642
+
643
+ history_path = self._get_chat_history_path()
644
+ with open(history_path, 'w', encoding='utf-8') as f:
645
+ json.dump(messages_to_save, f, ensure_ascii=False, indent=2)
646
+
647
+ except Exception as e:
648
+ logger.error(f"Failed to save chat history: {e}")
649
+
650
+ def _load_chat_history(self):
651
+ """チャット履歴を読み込む"""
652
+ try:
653
+ history_path = self._get_chat_history_path()
654
+ if history_path.exists():
655
+ with open(history_path, 'r', encoding='utf-8') as f:
656
+ saved_messages = json.load(f)
657
+
658
+ # 保存されたメッセージを復元
659
+ for msg in saved_messages:
660
+ self._append_message(msg["sender"], msg["message"])
661
+ # _append_messageがさらに履歴に追加するのを防ぐため、最後のものを削除
662
+ if self.chat_history and self.chat_history[-1] == msg:
663
+ self.chat_history.pop()
664
+
665
+ self.chat_history = saved_messages
666
+
667
+ if self.chat_history:
668
+ self._append_message("System", "Previous conversation restored", "info")
669
+
670
+ except Exception as e:
671
+ logger.error(f"Failed to load chat history: {e}")
672
+
673
+ def _on_destroy(self, event):
674
+ """ウィンドウが破棄される時のクリーンアップ"""
675
+ # チャット履歴を保存
676
+ self._save_chat_history()
677
+
678
+ # キューチェックを停止
679
+ if hasattr(self, '_queue_check_id'):
680
+ self.after_cancel(self._queue_check_id)
681
+
682
+ # 生成を停止
683
+ self._stop_generation = True
684
+
685
+ # LLMクライアントをシャットダウン
686
+ if self.llm_client:
687
+ self.llm_client.shutdown()