diff --git a/docs/jira-knowledge-guide.md b/docs/jira-knowledge-guide.md
index e79d792..47d96ed 100644
--- a/docs/jira-knowledge-guide.md
+++ b/docs/jira-knowledge-guide.md
@@ -34,6 +34,10 @@ Jira Project Knowledge enables Cowork to index Jira issues as searchable project
1. In the same Jira dialog, check **Enable Jira Project Knowledge**.
2. Enter **Project Mapping** in the format `cowork_project_id:JIRA_PROJECT_KEY`.
* Example: `proj-alpha:ALPHA, proj-beta:BETA`
+ * Click the **ⓘ** icon next to "Project Mapping" for detailed help on:
+ * **Project ID**: The Cowork project identifier (e.g., `cowork-local`). Find it in your current Cowork project settings.
+ * **Jira Key**: The Jira project key (e.g., `ALPHA` from issue `ALPHA-123`). Open any Jira issue to find it.
+ * **Common mistake**: Do not enter issue keys like `ABC-123`. Only enter the project key part `ABC`.
3. Click **Save**.
### 3.3 Initial Sync
diff --git a/i18n/libreoffice_view.py b/i18n/libreoffice_view.py
index 12e61af..1a82f8a 100644
--- a/i18n/libreoffice_view.py
+++ b/i18n/libreoffice_view.py
@@ -220,6 +220,106 @@ STRINGS: Dict[str, Dict[str, str]] = {
"en": "Enter base URL, email and API token first.",
"ja": "先にベースURL・メール・APIトークンを入力してください。",
"vi": "Hãy nhập Base URL, Email và API token trước."},
+ # ---- Jira Project Knowledge help tooltips ---------------------------------
+ "connectors.jira_kb_section": {
+ "en": "Project Knowledge",
+ "ja": "プロジェクトナレッジ",
+ "vi": "Project Knowledge"},
+ "connectors.jira_kb_enable": {
+ "en": "Enable Jira Project Knowledge",
+ "ja": "Jiraプロジェクトナレッジを有効化",
+ "vi": "Bật Jira Project Knowledge"},
+ "connectors.jira_kb_mapping_label": {
+ "en": "Project Mapping",
+ "ja": "プロジェクトマッピング",
+ "vi": "Ánh xạ Project"},
+ "connectors.jira_kb_project_id_title": {
+ "en": "What is Project ID?",
+ "ja": "Project IDとは?",
+ "vi": "Project ID là gì?"},
+ "connectors.jira_kb_jira_key_title": {
+ "en": "What is Jira Key?",
+ "ja": "Jira Keyとは?",
+ "vi": "Jira Key là gì?"},
+ "connectors.jira_kb_mapping_hint": {
+ "en": "Map Cowork projects to Jira project keys. Format: cowork_project_id:JIRA_KEY",
+ "ja": "CoworkプロジェクトをJiraプロジェクトキーにマッピング。形式: cowork_project_id:JIRA_KEY",
+ "vi": "Ánh xạ project Cowork với Jira project key. Định dạng: cowork_project_id:JIRA_KEY"},
+ "connectors.jira_kb_sync_now": {
+ "en": "Sync Now",
+ "ja": "今すぐ同期",
+ "vi": "Đồng bộ ngay"},
+ "connectors.jira_kb_not_configured": {
+ "en": "Not configured",
+ "ja": "未設定",
+ "vi": "Chưa cấu hình"},
+ "connectors.jira_kb_disabled": {
+ "en": "Disabled",
+ "ja": "無効",
+ "vi": "Đã tắt"},
+ "connectors.jira_kb_syncing": {
+ "en": "Syncing…",
+ "ja": "同期中…",
+ "vi": "Đang đồng bộ…"},
+ "connectors.jira_kb_project_id_help": {
+ "en": ("What is Project ID?
"
+ "Project ID is the identifier of a project in Cowork Local. "
+ "This value links knowledge from Jira to the correct project in Cowork.
"
+ "Where to find it:
"
+ "You can get the Project ID from the currently open project in Cowork "
+ "or from the current project configuration.
"
+ "Example: cowork-local
"
+ "Common mistake:
"
+ "Do not enter a Jira Project Key or Jira Issue Key here."),
+ "ja": ("Project IDとは?
"
+ "Project IDはCowork Local内のプロジェクト識別子です。"
+ "この値でJiraのナレッジをCoworkの正しいプロジェクトに紐付けます。
"
+ "確認方法:
"
+ "Coworkで開いているプロジェクト、または現在のプロジェクト設定から取得できます。
"
+ "例: cowork-local
"
+ "よくある間違い:
"
+ "ここにJiraプロジェクトキーやJira課題キーを入力しないでください。"),
+ "vi": ("Project ID là gì?
"
+ "Project ID là định danh của project trong Cowork Local. "
+ "Giá trị này dùng để gắn knowledge từ Jira với đúng project trong Cowork.
"
+ "Cách lấy:
"
+ "Bạn có thể lấy Project ID từ project đang mở trong Cowork "
+ "hoặc từ cấu hình project hiện tại.
"
+ "Ví dụ: cowork-local
"
+ "Lỗi thường gặp:
"
+ "Không nhập Jira Project Key hoặc Jira Issue Key vào ô này.")},
+ "connectors.jira_kb_jira_key_help": {
+ "en": ("What is Jira Key?
"
+ "Jira Key is the short code of a Jira project — not an issue code.
"
+ "Where to find it:
"
+ "Open any issue in Jira. If the issue code is ABC-123, then the Jira Key is ABC.
"
+ "You can also find it in Jira Project Settings.
"
+ "Example:
"
+ "Issue: ABC-123 → Jira Key: ABC
"
+ "Common mistake:
"
+ "Do not enter ABC-123. Only enter ABC."),
+ "ja": ("Jira Keyとは?
"
+ "Jira KeyはJiraプロジェクトの短いコードです。課題コードではありません。
"
+ "確認方法:
"
+ "Jiraで任意の課題を開きます。課題コードがABC-123なら、Jira KeyはABCです。
"
+ "Jiraプロジェクト設定でも確認できます。
"
+ "例:
"
+ "課題: ABC-123 → Jira Key: ABC
"
+ "よくある間違い:
"
+ "ABC-123と入力しないでください。ABCのみ入力します。"),
+ "vi": ("Jira Key là gì?
"
+ "Jira Key là mã ngắn của Jira project, không phải mã của một issue.
"
+ "Cách lấy:
"
+ "Mở một issue bất kỳ trong Jira. Nếu issue có mã ABC-123 thì Jira Key là ABC.
"
+ "Bạn cũng có thể xem Jira Key trong Project settings của Jira.
"
+ "Ví dụ:
"
+ "Issue: ABC-123 → Jira Key: ABC
"
+ "Lỗi thường gặp:
"
+ "Không nhập ABC-123. Chỉ nhập ABC.")},
+ "connectors.jira_kb_validation_issue_key": {
+ "en": "Looks like you entered an Issue Key. Enter only the project key part, e.g. ABC.",
+ "ja": "課題キーを入力したようです。プロジェクトキー部分のみを入力してください(例: ABC)。",
+ "vi": "Có vẻ bạn đã nhập Issue Key. Hãy nhập chỉ phần project key, ví dụ ABC."},
"tools_admin.jira_group": {"en": "Jira connection", "ja": "Jira 接続", "vi": "Kết nối Jira"},
"tools_admin.jira_hint": {
"en": "Connect once, then just paste a Jira issue link into Cowork or a Co4E step — the agent "
diff --git a/tests/ui/test_jira_knowledge_help_ux.py b/tests/ui/test_jira_knowledge_help_ux.py
new file mode 100644
index 0000000..6e1bc51
--- /dev/null
+++ b/tests/ui/test_jira_knowledge_help_ux.py
@@ -0,0 +1,193 @@
+"""UX tests for Jira Project Knowledge help tooltips and validation.
+
+Verifies that the JiraConnectDialog shows contextual help icons for
+Project ID and Jira Key, renders correct help text, supports keyboard
+accessibility, and validates common user mistakes (e.g. entering ABC-123
+instead of ABC).
+"""
+from __future__ import annotations
+
+import pytest
+
+
+class _Config:
+ """Minimal config stub for JiraConnectDialog."""
+
+ def __init__(self):
+ self.data = {
+ "jira": {"base_url": "", "email": "", "api_token": ""},
+ "jira_knowledge": {"enabled": False, "projects": {}},
+ }
+
+ def save(self):
+ pass
+
+
+class _Ctx:
+ def __init__(self):
+ self.config = _Config()
+
+ def save(self):
+ self.config.save()
+
+
+@pytest.fixture
+def dialog(qapp):
+ from cowork_local.ui.connectors_panel import JiraConnectDialog
+ ctx = _Ctx()
+ dlg = JiraConnectDialog(ctx)
+ yield dlg
+ dlg.deleteLater()
+
+
+# ---- Help icon presence ---------------------------------------------------
+
+def test_help_icon_exists(dialog):
+ """The mapping label row must contain a help button (?)."""
+ from PySide6.QtWidgets import QToolButton
+ dlg = dialog
+ help_icons = dlg.findChildren(QToolButton)
+ assert len(help_icons) >= 1, "Help button (?) not found in JiraConnectDialog"
+
+
+def test_help_icon_has_tooltip(dialog):
+ """Help icon must have a non-empty tooltip with both Project ID and Jira Key explanations."""
+ from PySide6.QtWidgets import QToolButton
+ dlg = dialog
+ help_icons = dlg.findChildren(QToolButton)
+ assert help_icons, "No help button found"
+ tooltip = help_icons[0].toolTip()
+ assert tooltip, "Help button has no tooltip"
+ assert "Project ID" in tooltip, "Tooltip missing Project ID explanation"
+ assert "Jira Key" in tooltip, "Tooltip missing Jira Key explanation"
+
+
+# ---- Help text content ----------------------------------------------------
+
+def test_project_id_help_content(dialog):
+ """Project ID help must explain what it is, where to find it, example, and common mistake."""
+ from cowork_local.i18n import tr
+ text = tr("connectors.jira_kb_project_id_help")
+ assert "cowork-local" in text.lower() or "Cowork" in text, "Missing example"
+ # Must warn against entering Jira keys
+ lower = text.lower()
+ assert "jira" in lower and ("key" in lower or "issue" in lower), \
+ "Missing common mistake warning about Jira keys"
+
+
+def test_jira_key_help_content(dialog):
+ """Jira Key help must include the ABC-123 → ABC example."""
+ from cowork_local.i18n import tr
+ text = tr("connectors.jira_kb_jira_key_help")
+ assert "ABC-123" in text, "Missing ABC-123 example"
+ assert "ABC" in text, "Missing ABC extraction example"
+
+
+def test_jira_key_help_warns_against_issue_key(dialog):
+ """Jira Key help must explicitly warn not to enter ABC-123."""
+ from cowork_local.i18n import tr
+ text = tr("connectors.jira_kb_jira_key_help")
+ lower = text.lower()
+ # Should contain a warning like "Do not enter ABC-123" or "Không nhập ABC-123"
+ assert "abc-123" in lower, "Missing warning about entering issue key format"
+
+
+# ---- Validation -----------------------------------------------------------
+
+def test_validation_shows_on_issue_key_pattern(dialog):
+ """Typing 'proj:ABC-123' should show the validation warning."""
+ dlg = dialog
+ dlg.show()
+ dlg.project_mapping.setText("myproject:ABC-123")
+ assert not dlg.mapping_validation.isHidden(), \
+ "Validation hint should be shown when Issue Key pattern detected"
+ assert dlg.mapping_validation.text(), "Validation hint should have text"
+
+
+def test_validation_hides_on_correct_input(dialog):
+ """Typing 'proj:ABC' should NOT show the validation warning."""
+ dlg = dialog
+ dlg.show()
+ dlg.project_mapping.setText("myproject:ABC")
+ assert dlg.mapping_validation.isHidden(), \
+ "Validation hint should be hidden for correct Jira Key format"
+
+
+def test_validation_hides_on_empty(dialog):
+ """Empty input should not show validation warning."""
+ dlg = dialog
+ dlg.show()
+ dlg.project_mapping.setText("")
+ assert dlg.mapping_validation.isHidden(), \
+ "Validation hint should be hidden on empty input"
+
+
+def test_validation_multiple_mappings(dialog):
+ """Validation should detect issue key pattern even in multi-mapping strings."""
+ dlg = dialog
+ dlg.show()
+ dlg.project_mapping.setText("proj-a:ALPHA, proj-b:DEF-456")
+ assert not dlg.mapping_validation.isHidden(), \
+ "Validation should trigger when any mapping contains an Issue Key pattern"
+
+
+# ---- Existing behavior unchanged ------------------------------------------
+
+def test_save_still_works(dialog):
+ """Saving with valid mapping still produces correct config structure."""
+ dlg = dialog
+ dlg.url.setText("https://example.atlassian.net")
+ dlg.email.setText("user@example.com")
+ dlg.token.setText("test-token")
+ dlg.project_mapping.setText("myproject:MYKEY")
+ dlg.kb_enabled.setChecked(True)
+ dlg._save()
+ jira_kb = dlg.ctx.config.data.get("jira_knowledge", {})
+ assert jira_kb["enabled"] is True
+ assert jira_kb["projects"] == {"myproject": "MYKEY"}
+
+
+def test_sync_button_present(dialog):
+ """Sync Now button must still exist and be functional."""
+ dlg = dialog
+ assert dlg.sync_btn is not None
+ assert dlg.sync_btn.text(), "Sync button should have text"
+
+
+# ---- Accessibility --------------------------------------------------------
+
+def test_help_icon_cursor(dialog):
+ """Help button should be a QToolButton (clickable by nature)."""
+ from PySide6.QtWidgets import QToolButton
+ dlg = dialog
+ help_icons = dlg.findChildren(QToolButton)
+ assert help_icons, "No help button found"
+ # QToolButton is inherently clickable, no need for cursor check
+ assert help_icons[0].text() == "?", "Help button should display '?' text"
+
+
+# ---- i18n keys exist for all three languages ------------------------------
+
+@pytest.mark.parametrize("lang", ["en", "vi", "ja"])
+def test_i18n_keys_exist(lang):
+ """All Jira KB help keys must have translations for en, vi, ja."""
+ from cowork_local.i18n import STRINGS
+ required_keys = [
+ "connectors.jira_kb_section",
+ "connectors.jira_kb_enable",
+ "connectors.jira_kb_mapping_label",
+ "connectors.jira_kb_project_id_title",
+ "connectors.jira_kb_jira_key_title",
+ "connectors.jira_kb_project_id_help",
+ "connectors.jira_kb_jira_key_help",
+ "connectors.jira_kb_validation_issue_key",
+ "connectors.jira_kb_sync_now",
+ "connectors.jira_kb_not_configured",
+ "connectors.jira_kb_disabled",
+ "connectors.jira_kb_syncing",
+ ]
+ for key in required_keys:
+ assert key in STRINGS, f"Missing i18n key: {key}"
+ entry = STRINGS[key]
+ assert lang in entry, f"Missing '{lang}' translation for key: {key}"
+ assert entry[lang], f"Empty '{lang}' translation for key: {key}"
\ No newline at end of file
diff --git a/ui/connectors_panel.py b/ui/connectors_panel.py
index e64fcff..009f10c 100644
--- a/ui/connectors_panel.py
+++ b/ui/connectors_panel.py
@@ -12,9 +12,11 @@ setup dialog; OneDrive/SharePoint: neither, they only toggle).
from __future__ import annotations
from PySide6.QtCore import Qt
+from PySide6.QtGui import QFont
from PySide6.QtWidgets import (
QCheckBox, QDialog, QFormLayout, QFrame, QGroupBox, QHBoxLayout, QLabel,
- QLineEdit, QMessageBox, QPushButton, QScrollArea, QVBoxLayout, QWidget,
+ QLineEdit, QMessageBox, QPushButton, QScrollArea, QToolButton, QToolTip,
+ QVBoxLayout, QWidget,
)
from ..core.ext_connectors import CATEGORIES as EXT_CATEGORIES
@@ -81,19 +83,19 @@ class JiraConnectDialog(QDialog):
main_layout.addWidget(conn_group)
# === Project Knowledge Section ===
- kb_group = QGroupBox("Project Knowledge")
+ kb_group = QGroupBox(tr("connectors.jira_kb_section"))
kb_layout = QVBoxLayout(kb_group)
- self.kb_enabled = QCheckBox("Enable Jira Project Knowledge")
+ self.kb_enabled = QCheckBox(tr("connectors.jira_kb_enable"))
self.kb_enabled.setChecked(jira_kb.get("enabled", False))
kb_layout.addWidget(self.kb_enabled)
- kb_hint = QLabel("Map Cowork projects to Jira project keys. Format: cowork_project_id:JIRA_KEY")
+ kb_hint = QLabel(tr("connectors.jira_kb_mapping_hint"))
kb_hint.setObjectName("hint")
kb_hint.setWordWrap(True)
kb_layout.addWidget(kb_hint)
- # Project mapping input
+ # Project mapping input with help icons for Project ID and Jira Key
mapping_form = QFormLayout()
self.project_mapping = QLineEdit()
# Load existing mappings
@@ -102,17 +104,67 @@ class JiraConnectDialog(QDialog):
mapping_str = ", ".join(f"{k}:{v}" for k, v in existing_projects.items())
self.project_mapping.setText(mapping_str)
self.project_mapping.setPlaceholderText("proj-alpha:ALPHA, proj-beta:BETA")
- mapping_form.addRow("Project Mapping", self.project_mapping)
+
+ # Label with help icon explaining both Project ID and Jira Key
+ mapping_label = QLabel(tr("connectors.jira_kb_mapping_label"))
+ self.help_icon = QToolButton()
+ self.help_icon.setText("?")
+ self.help_icon.setToolTip(
+ "" + tr("connectors.jira_kb_project_id_title") + "
"
+ + tr("connectors.jira_kb_project_id_help")
+ + "
" + tr("connectors.jira_kb_jira_key_title") + "
"
+ + tr("connectors.jira_kb_jira_key_help")
+ )
+ self.help_icon.setStyleSheet("""
+ QToolButton {
+ border: 1px solid #5B9BD5;
+ border-radius: 10px;
+ background: transparent;
+ color: #5B9BD5;
+ font-weight: bold;
+ padding: 2px 6px;
+ min-width: 18px;
+ min-height: 18px;
+ }
+ QToolButton:hover {
+ background: #5B9BD5;
+ color: white;
+ }
+ QToolButton:pressed {
+ background: #4A8BC7;
+ color: white;
+ }
+ """)
+ # Native Qt tooltip: hover to show, no click handler needed.
+ # AutoRaise=False keeps the button always visible (not faded out).
+ self.help_icon.setAutoRaise(False)
+ label_row = QHBoxLayout()
+ label_row.setSpacing(4)
+ label_row.addWidget(mapping_label)
+ label_row.addWidget(self.help_icon)
+ label_row.addStretch(1)
+ label_widget = QWidget()
+ label_widget.setLayout(label_row)
+ mapping_form.addRow(label_widget, self.project_mapping)
+
+ # Validation hint for common mistakes (e.g., entering ABC-123 instead of ABC)
+ self.mapping_validation = QLabel()
+ self.mapping_validation.setObjectName("hint")
+ self.mapping_validation.setWordWrap(True)
+ self.mapping_validation.hide()
+ mapping_form.addRow("", self.mapping_validation)
+ self.project_mapping.textChanged.connect(self._validate_mapping)
+
kb_layout.addLayout(mapping_form)
# Sync controls
sync_row = QHBoxLayout()
- self.sync_btn = QPushButton("Sync Now")
+ self.sync_btn = QPushButton(tr("connectors.jira_kb_sync_now"))
self.sync_btn.clicked.connect(self._trigger_sync)
self.sync_btn.setEnabled(False)
sync_row.addWidget(self.sync_btn)
- self.sync_status = QLabel("Not configured")
+ self.sync_status = QLabel(tr("connectors.jira_kb_not_configured"))
self.sync_status.setObjectName("hint")
sync_row.addWidget(self.sync_status)
sync_row.addStretch(1)
@@ -136,15 +188,36 @@ class JiraConnectDialog(QDialog):
self.kb_enabled.toggled.connect(self._update_sync_state)
self._update_sync_state(self.kb_enabled.isChecked())
+ def _on_paste(self, text: str) -> None:
+ """Auto-fill Base URL from a pasted Jira link."""
+ import re
+ # Extract base URL from patterns like https://example.atlassian.net/browse/ABC-123
+ match = re.search(r"(https?://[^/\s]+\.atlassian\.net)", text.strip())
+ if match and not self.url.text().strip():
+ self.url.setText(match.group(1))
+
def _update_sync_state(self, enabled: bool) -> None:
"""Enable/disable sync controls based on KB checkbox."""
self.sync_btn.setEnabled(enabled)
if not enabled:
- self.sync_status.setText("Disabled")
+ self.sync_status.setText(tr("connectors.jira_kb_disabled"))
+
+ def _validate_mapping(self, text: str) -> None:
+ """Show inline hint if user appears to enter an Issue Key (ABC-123) instead of just Jira Key (ABC)."""
+ import re
+ # Detect pattern like "proj:ABC-123" or "proj:ABC-123, proj2:DEF-456"
+ # A Jira project key should be uppercase letters only (e.g., ABC), not ABC-123
+ issue_key_pattern = re.compile(r':\s*[A-Z]+-\d+')
+ if issue_key_pattern.search(text):
+ self.mapping_validation.setText(tr("connectors.jira_kb_validation_issue_key"))
+ self.mapping_validation.setStyleSheet("color: #D4A017; font-style: italic;")
+ self.mapping_validation.show()
+ else:
+ self.mapping_validation.hide()
def _trigger_sync(self) -> None:
"""Trigger a background sync job using JiraSyncService."""
- self.sync_status.setText("Syncing...")
+ self.sync_status.setText(tr("connectors.jira_kb_syncing"))
self.sync_btn.setEnabled(False)
def job(_w):