CI / test (push) Canceled after 0s
## Summary epic r04 - begin refactor ## Change Type - [x] Cowork feature - [ ] Bug fix - [ ] Core AI contribution - [ ] Test / hardening - [ ] Performance - [ ] Documentation ## Related Work Cowork Task: Core Repo: http://34.143.229.138/gitea-admin/fsg-ai-core-assets Core AI Issue: Core Task: Related PR: ## Scope What is intentionally included? What is intentionally NOT included? ## Validation - [ ] Unit tests - [ ] Integration tests - [ ] Manual verification - [ ] Regression check Commands / evidence: ## Security Impact Permission / credential / network / customer data impact: ## Compatibility - [ ] No breaking change - [ ] Breaking change documented ## Reviewer Notes Anything Cowork reviewers should pay attention to. --------- Co-authored-by: Anh Tran Nguyen Minh <anhtnm1@fpt.com> Co-authored-by: Huong Le Thi Thien <huongltt35@fpt.com> Co-authored-by: Nam Pham Dinh Thanh <nampdt@fpt.com> Co-authored-by: Vu Dam Tuan <vudt15@fpt.com> Co-authored-by: Hiep Ha Van <hiephv3@fpt.com> Co-authored-by: Lam Hoang Van <lamhv7@fpt.com> Reviewed-on: #7 Co-authored-by: Duy Le Huu <duylh19@fpt.com>
151 lines
7.8 KiB
Python
151 lines
7.8 KiB
Python
"""Bố cục khung chat — R08-T06.
|
||
|
||
Hai cột: mạch hội thoại bên trái, khung tệp đầu ra bên phải. Ô nhập nằm dưới
|
||
CẢ HAI cột — đó là lý do khung tệp đứng cạnh mạch hội thoại mà không làm hẹp
|
||
chỗ gõ. Đặt trong cột chat thì ô nhập co lại mỗi lần có tệp xuất hiện.
|
||
|
||
Vài widget cố ý được gắn vào một cha ẩn vĩnh viễn thay vì bỏ hẳn: khung tệp
|
||
đầu vào và bảng kế hoạch cũ vẫn còn được gọi ``set_steps``/``add`` ở nơi
|
||
khác. Không có cha thì lần gọi đầu tiên sẽ bật lên thành một cửa sổ nổi lạc
|
||
lõng giữa màn hình.
|
||
|
||
Cùng kiểu mixin, xem ghi chú ở đầu ``presentation/shell/nav_rail.py``.
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
from pathlib import Path
|
||
from typing import Any, Dict, List, Optional
|
||
from PySide6.QtCore import Qt, QTimer, Signal
|
||
from PySide6.QtCore import QFileSystemWatcher
|
||
from PySide6.QtWidgets import (
|
||
QComboBox, QHBoxLayout, QLabel, QMessageBox, QPushButton, QSplitter,
|
||
QVBoxLayout, QWidget,
|
||
)
|
||
from ...core.worker import AgentWorker
|
||
from ...i18n import on_language_changed, tr
|
||
from ...state import AppContext
|
||
from ...theme import current_palette
|
||
from .chat_bubble_style import ThinkingIndicator
|
||
from .chat_history_widget import ChatView
|
||
from .composer_widget import Composer
|
||
from ...ui.icons import collapse_right_icon, icon as app_icon
|
||
from ...ui.osutil import is_image, open_path
|
||
from ...ui.widgets import CollapsibleSection, CollapseStrip, PlanSection
|
||
|
||
|
||
class ChatPanelLayoutMixin:
|
||
"""Dựng bố cục. Trộn vào ChatPanel."""
|
||
|
||
def _build_layout(self, root) -> None:
|
||
"""``root`` là QVBoxLayout gốc do ``__init__`` dựng."""
|
||
# Chat column: transcript expands, the chat box is pinned at the bottom.
|
||
chat_col = QWidget()
|
||
cc = QVBoxLayout(chat_col)
|
||
cc.setContentsMargins(0, 0, 0, 0)
|
||
cc.setSpacing(0)
|
||
cc.addWidget(self.chat_view, 1)
|
||
self.thinking = ThinkingIndicator() # animated "working…" line while we wait
|
||
cc.addWidget(self.thinking)
|
||
self.center_split = QSplitter(Qt.Horizontal)
|
||
self.center_split.addWidget(chat_col)
|
||
root.addWidget(self.center_split, 1)
|
||
|
||
# The composer spans the whole screen, under BOTH columns — that is how
|
||
# the drawing lays it out, and it is the reason the files panel can sit
|
||
# beside the transcript without narrowing what you type into. Inside the
|
||
# chat column it stopped at the panel's edge and the input shrank
|
||
# whenever files appeared.
|
||
composer_wrap = QWidget()
|
||
cwl = QVBoxLayout(composer_wrap)
|
||
cwl.setContentsMargins(8, 4, 8, 8)
|
||
cwl.addWidget(self.composer)
|
||
root.addWidget(composer_wrap)
|
||
|
||
# Right sidebar: Output files only (see below — Input is tracked but
|
||
# not shown).
|
||
self.input_section = CollapsibleSection(tr("widgets.input_files"))
|
||
# No cap: this section owns the whole right panel (its header is
|
||
# hoisted into io_hdr below), so the list should fill the space down
|
||
# to the composer instead of stopping at a fixed height with empty
|
||
# panel below it.
|
||
self.output_section = CollapsibleSection(tr("widgets.output_files").upper(), max_height=None)
|
||
# Input files are NOT shown in Cowork's UI anymore — but they're still
|
||
# fully tracked (add/remove/paths()) exactly as before, since that list
|
||
# is what gets written into the conversation's own "inputs" field on
|
||
# save (kept alongside the conversation; nothing here deletes the
|
||
# user's actual files — the conversation JSON itself only disappears
|
||
# when the conversation is deleted, same as always). Give input_section
|
||
# a real, permanently-hidden PARENT (not just "never added to a layout")
|
||
# so its own internal auto-show-on-add() call can never pop it up as a
|
||
# stray floating window.
|
||
self._input_hidden_host = QWidget(self)
|
||
self._input_hidden_host.setVisible(False)
|
||
_hh_lay = QVBoxLayout(self._input_hidden_host)
|
||
_hh_lay.setContentsMargins(0, 0, 0, 0)
|
||
_hh_lay.addWidget(self.input_section)
|
||
self.plan_section = PlanSection(tr("widgets.plan_title")) # live step checklist, above the Files panel
|
||
# The plan is shown INLINE in the conversation now (see add_plan), so this
|
||
# legacy right-panel checklist is parked inside the permanently-hidden
|
||
# host. Without a parent it would pop as a stray top-level "Plan (N)"
|
||
# window the moment set_steps() made it visible — parenting it here keeps
|
||
# its set_steps/clear calls truly inert (a hidden ancestor never renders).
|
||
_hh_lay.addWidget(self.plan_section)
|
||
self.input_section.activated.connect(self._open_io_item)
|
||
self.output_section.activated.connect(self._open_io_item)
|
||
# Right-click a file → Open / "View & AI Edit" (in-app viewer+editor).
|
||
for section in (self.input_section, self.output_section):
|
||
section.list.setContextMenuPolicy(Qt.CustomContextMenu)
|
||
section.list.customContextMenuRequested.connect(
|
||
lambda pos, s=section: self._io_context_menu(s, pos))
|
||
self._io_widget = QWidget()
|
||
iol = QVBoxLayout(self._io_widget)
|
||
iol.setContentsMargins(6, 6, 6, 6)
|
||
iol.setSpacing(4)
|
||
io_hdr = QHBoxLayout()
|
||
self._io_collapse_btn = QPushButton()
|
||
self._io_collapse_btn.setIcon(collapse_right_icon())
|
||
self._io_collapse_btn.setFixedWidth(28)
|
||
self._io_collapse_btn.clicked.connect(lambda: self._set_io_collapsed(True))
|
||
self._files_header = QLabel()
|
||
self._files_header.setStyleSheet("font-weight:600;")
|
||
# The drawing gives this panel ONE heading — "TỆP ĐẦU RA (3) ›" — and
|
||
# the section already draws exactly that, count included. A separate
|
||
# "Files" label above it was the same thing said twice, so the section's
|
||
# own header moves onto this row and the collapse chevron sits at its
|
||
# right, where the drawing puts it. _files_header stays for the tabs
|
||
# that still label their panel, just not in this layout.
|
||
self._files_header.setVisible(False)
|
||
io_hdr.addWidget(self.output_section.header, 1)
|
||
io_hdr.addWidget(self._io_collapse_btn)
|
||
# The plan now shows INLINE in the conversation (an expandable block whose
|
||
# steps tick off as they complete), not in this right panel — so it's kept
|
||
# out of the layout here. The object stays (its set_steps/clear calls are
|
||
# harmless no-ops on a hidden widget).
|
||
self.plan_section.setVisible(False)
|
||
iol.addLayout(io_hdr)
|
||
bl_host = QWidget()
|
||
bl = QVBoxLayout(bl_host)
|
||
bl.setContentsMargins(0, 0, 0, 0)
|
||
bl.setSpacing(4)
|
||
bl.addWidget(self.output_section, 1) # Output only — Input is tracked but hidden; fills down to the composer
|
||
iol.addWidget(bl_host, 1)
|
||
|
||
# Collapsing shrinks the panel to a thin clickable line (not hidden).
|
||
# The collapse button lives in the panel header; the strip re-expands.
|
||
self._io_strip = CollapseStrip(tr("chatpanel.expand_files_tooltip"), expand_dir="left")
|
||
self._io_strip.clicked.connect(lambda: self._set_io_collapsed(False))
|
||
self._io_strip.setVisible(False)
|
||
self._io_pane = QWidget()
|
||
pl = QHBoxLayout(self._io_pane)
|
||
pl.setContentsMargins(0, 0, 0, 0)
|
||
pl.setSpacing(0)
|
||
pl.addWidget(self._io_strip)
|
||
pl.addWidget(self._io_widget, 1)
|
||
|
||
self.center_split.addWidget(self._io_pane)
|
||
self.center_split.setStretchFactor(0, 1)
|
||
self.center_split.setStretchFactor(1, 0)
|
||
self.center_split.setChildrenCollapsible(False)
|
||
self.center_split.setSizes([820, 220])
|
||
on_language_changed(self._retranslate_base)
|