fix(chat): tệp đính kèm không bị bản sao trong thư mục lấn chỗ
Người dùng phản ánh dán nội dung vào chat cho câu trả lời tốt hơn đính kèm cùng nội dung đó. Nội dung KHÔNG bị cắt (giới hạn 2 triệu ký tự) — nguyên nhân là loãng và trùng: _augment luôn quét thêm [Workspace files] và [Project files], và tệp đính kèm nếu nằm trong thư mục workspace sẽ đi vào prompt HAI lần. Với tài liệu dài, bản thứ hai vừa nhân đôi ngữ cảnh vừa khiến model không biết bản nào là bản được hỏi. Lọc trùng theo đường dẫn đã giải quyết, và đổi nhãn để nói rõ tệp đính kèm là CHỦ THỂ CHÍNH còn tệp thư mục chỉ là ngữ cảnh phụ. Dòng cảnh báo "N tệp không nạp được" đếm TRƯỚC khi lọc trùng, nếu không nó báo sai. Cùng lượt, hai chỗ bỏ thông tin không cần thiết: - gỡ dòng "provider · model" ngay sau chữ "Cowork" — nó lặp lại thứ bộ chọn provider ở thanh trên đang hiển thị, mà chiếm chỗ đắt nhất trên thanh công cụ; - không báo "đang dùng <provider>" ở thanh trạng thái khi đổi provider — bộ chọn nằm ngay trên màn hình và đã hiện thứ người dùng vừa tự chọn. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -59,9 +59,19 @@ class AttachmentMixin:
|
||||
lines = [text] if text else []
|
||||
|
||||
# --- User-attached files ---
|
||||
# Đường dẫn đã giải quyết của các tệp đính kèm, để vòng quét thư mục
|
||||
# phía sau không gửi lại chính chúng một lần nữa.
|
||||
da_dinh_kem = set()
|
||||
if has_attachments:
|
||||
lines.append("\n[Attachments] — read and use these files to answer the request:")
|
||||
lines.append(
|
||||
"\n[Attachments] — the user attached these files for THIS request. "
|
||||
"They are the PRIMARY subject: read them in full and base the answer "
|
||||
"on them. Anything listed further below is background context only.")
|
||||
for p in attachments:
|
||||
try:
|
||||
da_dinh_kem.add(str(Path(p).resolve()))
|
||||
except OSError:
|
||||
pass
|
||||
lines.extend(self._read_one_attachment(p, limit, notify))
|
||||
|
||||
# --- Auto-load existing workspace/output folder files as input data ---
|
||||
@@ -74,10 +84,10 @@ class AttachmentMixin:
|
||||
if workspace is not None:
|
||||
lines.extend(self._folder_input_lines(
|
||||
workspace,
|
||||
"[Workspace files] — existing files in output folder, "
|
||||
"read and use as input data. The user expects you to "
|
||||
"process these files automatically:",
|
||||
limit, max_files, notify))
|
||||
"[Workspace files] — other files that happen to sit in the output "
|
||||
"folder. Background context; do NOT let them displace the "
|
||||
"attached files or the user's own question:",
|
||||
limit, max_files, notify, da_dinh_kem))
|
||||
|
||||
# --- Project knowledge (Claude-Projects style) ---
|
||||
# Only scanned separately when it's a DIFFERENT folder from the
|
||||
@@ -88,31 +98,44 @@ class AttachmentMixin:
|
||||
if knowledge is not None and knowledge != workspace:
|
||||
lines.extend(self._folder_input_lines(
|
||||
knowledge,
|
||||
"[Project files] — shared knowledge files of this project, "
|
||||
"available to every conversation in it. Read and use them "
|
||||
"as context for the request:",
|
||||
limit, max_files, notify))
|
||||
"[Project files] — shared knowledge of this project. Background "
|
||||
"context; do NOT let them displace the attached files or "
|
||||
"the user's own question:",
|
||||
limit, max_files, notify, da_dinh_kem))
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def _folder_input_lines(self, folder: Path, header: str, limit: int,
|
||||
max_files: int, notify=None) -> list:
|
||||
max_files: int, notify=None, skip=frozenset()) -> list:
|
||||
"""Embed a folder's readable files into the prompt — recursing into
|
||||
every sub-folder, any depth, not just the top level, so files placed
|
||||
in nested folders are read and processed too (same per-message file
|
||||
cap as manual attachments — Settings → Attachments → max files;
|
||||
0 = unlimited — so a folder with dozens of files can't blow the
|
||||
context window)."""
|
||||
from pathlib import Path as _P
|
||||
|
||||
from ...core.doc_extract import find_input_files
|
||||
|
||||
out: list = []
|
||||
shown, total = find_input_files(folder, self._INPUT_EXTS, max_files)
|
||||
# Bo qua tep nguoi dung DA dinh kem tuong minh. Tep dinh kem thuong nam
|
||||
# ngay trong thu muc workspace, nen khong loc thi cung mot tai lieu di vao
|
||||
# prompt HAI lan: mot lan duoi [Attachments], mot lan duoi [Workspace
|
||||
# files]. Voi tai lieu dai, ban thu hai vua nhan doi ngu canh vua khien
|
||||
# model khong biet ban nao la ban duoc hoi.
|
||||
# Số tệp thư mục này thực sự trả về, ĐO TRƯỚC khi lọc trùng: dòng cảnh
|
||||
# báo bên dưới nói về giới hạn mỗi lượt, nên đếm cả tệp bị lọc vì đã
|
||||
# đính kèm sẽ báo sai là "không nạp được".
|
||||
so_lay_duoc = len(shown)
|
||||
if skip:
|
||||
shown = [f for f in shown if str(_P(f).resolve()) not in skip]
|
||||
if shown:
|
||||
out.append("\n" + header)
|
||||
for f in shown:
|
||||
out.extend(self._read_one_attachment(str(f), limit, notify))
|
||||
if total > len(shown):
|
||||
skipped = total - len(shown)
|
||||
if total > so_lay_duoc:
|
||||
skipped = total - so_lay_duoc
|
||||
out.append(f"…({skipped} more files in the folder were not "
|
||||
"loaded — per-message attachment limit; mention a "
|
||||
"file by name if the user asks about it)")
|
||||
|
||||
@@ -191,10 +191,9 @@ class TopBarMixin:
|
||||
# Reload the Cowork tab's Agent (Model) list for the newly selected provider.
|
||||
self.cowork.refresh_agents()
|
||||
self.workspace.refresh_ai_models() # + the Folder AI-edit model picker
|
||||
self.statusBar().showMessage(
|
||||
tr("app.status.using_provider",
|
||||
label=PROVIDER_LABELS.get(self.ctx.config.active_provider))
|
||||
)
|
||||
# Khong bao "dang dung <provider>" o thanh trang thai: chinh bo chon
|
||||
# provider nam ngay tren man hinh va da hien thu vua chon, nen dong thong
|
||||
# bao chi nhac lai mot thu nguoi dung vua tu tay lam.
|
||||
def _on_language_changed(self, _idx: int) -> None:
|
||||
"""Đổi ngôn ngữ giao diện; trùng ngôn ngữ hiện tại thì bỏ qua để không dựng lại
|
||||
toàn bộ chữ vô ích.
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
"""Sáu chỗ chỉnh nhỏ trên giao diện, mỗi bài chốt đúng một triệu chứng đã báo.
|
||||
|
||||
Gom một file vì chúng không chia sẻ gì ngoài việc đều là phản hồi từ người dùng
|
||||
trong cùng một vòng; tách sáu file cho sáu khẳng định chỉ tạo thêm chỗ để tìm.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
REPO = Path(__file__).resolve().parents[2]
|
||||
|
||||
|
||||
# ---- bỏ nhãn "provider · model" cạnh chữ Cowork --------------------------
|
||||
|
||||
def test_thanh_cong_cu_cowork_khong_con_nhan_provider():
|
||||
"""Nó lặp lại thứ bộ chọn provider ở thanh trên đang hiển thị."""
|
||||
src = (REPO / "ui" / "cowork_tab.py").read_text(encoding="utf-8")
|
||||
|
||||
assert "model_lbl" not in src
|
||||
assert "PROVIDER_LABELS" not in src, "import đã thành vô dụng thì phải gỡ"
|
||||
|
||||
|
||||
# ---- không báo trạng thái khi đổi provider -------------------------------
|
||||
|
||||
def test_doi_provider_khong_bao_o_thanh_trang_thai():
|
||||
"""Bộ chọn nằm ngay trên màn hình và đã hiện thứ vừa chọn."""
|
||||
src = (REPO / "presentation" / "shell" / "top_bar.py").read_text(encoding="utf-8")
|
||||
than_ham = src.split("def _on_provider_changed")[1].split("def _on_language_changed")[0]
|
||||
|
||||
assert "showMessage" not in than_ham
|
||||
assert "using_provider" not in than_ham
|
||||
|
||||
|
||||
# ---- tên project mặc định không gắn ngôn ngữ ----------------------------
|
||||
|
||||
def test_ten_project_mac_dinh_khong_qua_tr():
|
||||
"""Tên project được GHI XUỐNG ĐĨA.
|
||||
|
||||
Tạo project lúc đang ở tiếng Nhật thì tên nó thành "新規プロジェクト" vĩnh
|
||||
viễn, và đổi ngôn ngữ về tiếng Việt không sửa được — đó là dữ liệu, không
|
||||
phải chữ giao diện. Người dùng nhìn thấy chữ Nhật trên màn tiếng Việt và
|
||||
tưởng là lỗi hiển thị.
|
||||
"""
|
||||
from cowork_local.presentation.workspace import project_editing as pe
|
||||
|
||||
assert pe._DEFAULT_PROJECT_NAME.isascii(), "tên mặc định phải trung tính"
|
||||
src = (REPO / "presentation" / "workspace" / "project_editing.py").read_text(encoding="utf-8")
|
||||
than_ham = src.split("def _create")[1].split("def _delete")[0]
|
||||
assert 'tr("workspace.default_new_name")' not in than_ham
|
||||
|
||||
|
||||
def test_ten_mac_dinh_khong_trung_nhau(monkeypatch):
|
||||
"""Bấm "Project mới" hai lần liên tiếp không được ra hai tên giống nhau."""
|
||||
from cowork_local.presentation.workspace.project_editing import (
|
||||
ProjectEditingMixin, _DEFAULT_PROJECT_NAME,
|
||||
)
|
||||
import cowork_local.core.projects as projects
|
||||
|
||||
class _P:
|
||||
def __init__(self, pid, name):
|
||||
self.project_id, self.name = pid, name
|
||||
|
||||
da_co = [_P("p1", _DEFAULT_PROJECT_NAME)]
|
||||
monkeypatch.setattr(projects, "list_projects", lambda: da_co)
|
||||
|
||||
class _K:
|
||||
_name_taken = ProjectEditingMixin._name_taken
|
||||
|
||||
assert _K()._name_taken(_DEFAULT_PROJECT_NAME) is True
|
||||
assert _K()._name_taken(f"{_DEFAULT_PROJECT_NAME} (2)") is False
|
||||
|
||||
|
||||
# ---- "Tất cả project…" phải hiện MỌI project ----------------------------
|
||||
|
||||
def test_tat_ca_project_xoa_bo_loc_theo_project():
|
||||
"""Bảng lịch sử nhúng trong Cowork của MỘT project nên bị lọc theo project đó.
|
||||
|
||||
Vào bằng link "Tất cả project…" mà còn bộ lọc thì tạo 5 project chỉ thấy 1.
|
||||
"""
|
||||
src = (REPO / "presentation" / "shell" / "rail_project.py").read_text(encoding="utf-8")
|
||||
than_ham = src.split("def goto_all_projects")[1].split("def _on_rail_recent")[0]
|
||||
|
||||
assert 'set_project_filter("")' in than_ham
|
||||
|
||||
|
||||
# ---- ba nút quản lý project về cùng một hàng ----------------------------
|
||||
|
||||
def test_sua_va_luu_project_cung_hang_voi_project_moi(qapp, tmp_path):
|
||||
"""Trước đó "Lưu project" nằm dưới cùng khung bên phải, cách "Project mới"
|
||||
gần hết chiều cao màn hình."""
|
||||
from cowork_local.presentation.shell.bootstrap import build_config, build_context
|
||||
from cowork_local.presentation.shell.main_window import MainWindow
|
||||
from cowork_local.presentation.workspace.project_editing import _row_layout_of
|
||||
|
||||
config_path = tmp_path / "config.json"
|
||||
build_config(config_path)
|
||||
win = MainWindow(build_context(config_path))
|
||||
try:
|
||||
ws = win.workspace
|
||||
hang = _row_layout_of(ws._new_btn)
|
||||
|
||||
assert hang is not None, "không tìm được hàng chứa nút Project mới"
|
||||
assert _row_layout_of(ws._edit_btn) is hang
|
||||
assert _row_layout_of(ws._save_btn) is hang
|
||||
# Thu tu doc tu trai sang: tao moi -> sua -> luu
|
||||
assert (hang.indexOf(ws._new_btn)
|
||||
< hang.indexOf(ws._edit_btn)
|
||||
< hang.indexOf(ws._save_btn))
|
||||
finally:
|
||||
win.close()
|
||||
|
||||
|
||||
def test_hai_nut_project_khong_hien_ngoai_sub_tab_project(qapp, tmp_path):
|
||||
"""Hàng tiêu đề vắt ngang CẢ màn Workspace.
|
||||
|
||||
Chuyển "Sửa project" + "Lưu project" lên đó (bug 10) làm chúng hiện luôn ở
|
||||
Cowork, Co4E, Thư mục và GraphRAG — nơi không có biểu mẫu project nào để sửa
|
||||
hay lưu. Đúng luật mà ``_new_btn`` đã theo từ trước.
|
||||
"""
|
||||
from cowork_local.presentation.shell.bootstrap import build_config, build_context
|
||||
from cowork_local.presentation.shell.main_window import MainWindow
|
||||
|
||||
config_path = tmp_path / "config.json"
|
||||
build_config(config_path)
|
||||
win = MainWindow(build_context(config_path))
|
||||
try:
|
||||
ws = win.workspace
|
||||
ws._current_id = "gia-lap"
|
||||
|
||||
ws.tabs.setCurrentIndex(ws._project_tab_idx)
|
||||
ws._sync_project_buttons()
|
||||
assert ws._edit_btn.isHidden() is False
|
||||
assert ws._save_btn.isHidden() is False
|
||||
|
||||
if ws._cowork_tab_idx < 0:
|
||||
pytest.skip("bản dựng này không có sub-tab Cowork")
|
||||
ws.tabs.setTabVisible(ws._cowork_tab_idx, True)
|
||||
ws.tabs.setCurrentIndex(ws._cowork_tab_idx)
|
||||
ws._sync_project_buttons()
|
||||
|
||||
assert ws._edit_btn.isHidden() is True, "nút Sửa project lọt sang tab Cowork"
|
||||
assert ws._save_btn.isHidden() is True, "nút Lưu project lọt sang tab Cowork"
|
||||
finally:
|
||||
win.close()
|
||||
|
||||
|
||||
def test_chua_chon_project_thi_hai_nut_cung_an(qapp, tmp_path):
|
||||
"""Không có project nào đang mở thì cả Sửa lẫn Lưu đều vô nghĩa."""
|
||||
from cowork_local.presentation.shell.bootstrap import build_config, build_context
|
||||
from cowork_local.presentation.shell.main_window import MainWindow
|
||||
|
||||
config_path = tmp_path / "config.json"
|
||||
build_config(config_path)
|
||||
win = MainWindow(build_context(config_path))
|
||||
try:
|
||||
ws = win.workspace
|
||||
ws._current_id = ""
|
||||
ws.tabs.setCurrentIndex(ws._project_tab_idx)
|
||||
ws._sync_project_buttons()
|
||||
|
||||
assert ws._edit_btn.isHidden() is True
|
||||
assert ws._save_btn.isHidden() is True
|
||||
finally:
|
||||
win.close()
|
||||
+6
-9
@@ -7,7 +7,6 @@ from PySide6.QtWidgets import (
|
||||
QFileDialog, QHBoxLayout, QLabel, QPushButton, QVBoxLayout, QWidget,
|
||||
)
|
||||
|
||||
from ..config import PROVIDER_LABELS
|
||||
from ..core import agent_roles
|
||||
from ..core.worker import AgentWorker
|
||||
from ..i18n import on_language_changed, tr
|
||||
@@ -32,9 +31,6 @@ class CoworkTab(ChatPanel):
|
||||
|
||||
self._title_lbl = QLabel()
|
||||
self._title_lbl.setStyleSheet("font-weight:700; font-size:15px;")
|
||||
self.model_lbl = QLabel("")
|
||||
self.model_lbl.setObjectName("hint")
|
||||
|
||||
self.skills_btn = QPushButton()
|
||||
self.skills_btn.setIcon(icon("book"))
|
||||
self.skills_btn.clicked.connect(self._open_skills_manager)
|
||||
@@ -44,7 +40,6 @@ class CoworkTab(ChatPanel):
|
||||
self._new_btn.clicked.connect(self.new_session)
|
||||
|
||||
self.toolbar_layout.addWidget(self._title_lbl)
|
||||
self.toolbar_layout.addWidget(self.model_lbl)
|
||||
self.toolbar_layout.addStretch(1)
|
||||
self.toolbar_layout.addWidget(self.skills_btn)
|
||||
self.toolbar_layout.addWidget(self._new_btn)
|
||||
@@ -377,10 +372,12 @@ class CoworkTab(ChatPanel):
|
||||
self._apply_output_folder_label()
|
||||
|
||||
def refresh_header(self) -> None:
|
||||
"""Cập nhật dòng "provider · model" trên thanh công cụ Cowork."""
|
||||
cfg = self.ctx.config
|
||||
label = PROVIDER_LABELS.get(cfg.active_provider, cfg.active_provider)
|
||||
self.model_lbl.setText(f"{label} · {cfg.model_label()}")
|
||||
"""Cập nhật các nhãn trên thanh công cụ Cowork.
|
||||
|
||||
Dòng "provider · model" từng nằm ngay sau chữ "Cowork" đã được gỡ: nó
|
||||
lặp lại thông tin mà bộ chọn provider ở thanh trên đang hiển thị, và
|
||||
chiếm chỗ đắt nhất trên thanh công cụ cho một thứ chỉ để đọc.
|
||||
"""
|
||||
self._apply_output_folder_label() # picks up edits made via Settings too
|
||||
|
||||
def build_job(self, text: str, messages, out_dir):
|
||||
|
||||
Reference in New Issue
Block a user