Compare commits
32
Commits
@@ -39,3 +39,32 @@ jobs:
|
||||
|
||||
- name: Run tests
|
||||
run: python -m pytest tests -q
|
||||
|
||||
# --- CASAN Verification Gate -------------------------------------
|
||||
# Ba check này là điều kiện của cổng ngày 30/08. Chạy trên MỌI PR để
|
||||
# biết vi phạm ngay hôm phát sinh, thay vì dồn tới ngày cổng.
|
||||
#
|
||||
# Check 1 do Team Gamma sở hữu và đã có. Check 2 (Team Hoa) và Check 3
|
||||
# (Team Duy) chưa viết — bước dưới bỏ qua nếu script chưa tồn tại, để
|
||||
# thêm cổng không làm đỏ CI của hai team kia.
|
||||
|
||||
- name: "CASAN Check 1 — không có credential lộ (Team Gamma)"
|
||||
run: |
|
||||
python scripts/audit_security.py --self-test
|
||||
python scripts/audit_security.py
|
||||
|
||||
- name: "CASAN Check 2 — file production ≤ 400 dòng (Team Hoa)"
|
||||
run: |
|
||||
if [ -f scripts/check_loc.py ]; then
|
||||
python scripts/check_loc.py
|
||||
else
|
||||
echo "scripts/check_loc.py chưa có — Team Hoa viết, hạn 30/08. Bỏ qua."
|
||||
fi
|
||||
|
||||
- name: "CASAN Check 3 — domain/ và application/ không import PySide6 (Team Duy)"
|
||||
run: |
|
||||
if [ -f scripts/check_imports.py ]; then
|
||||
python scripts/check_imports.py
|
||||
else
|
||||
echo "scripts/check_imports.py chưa có — Team Duy viết, hạn 30/08. Bỏ qua."
|
||||
fi
|
||||
|
||||
+11
-5
@@ -28,7 +28,10 @@ bower_components/
|
||||
.env.preview
|
||||
*.pem
|
||||
*.key
|
||||
secrets/
|
||||
# Neo vào gốc repo: mẫu không neo nuốt MỌI thư mục tên secrets ở mọi độ
|
||||
# sâu — nó đã âm thầm chặn infrastructure/secrets/ (mã nguồn, không phải
|
||||
# bí mật) khỏi repo suốt 21-22/08.
|
||||
/secrets/
|
||||
credentials.json
|
||||
.npmrc
|
||||
.yarnrc
|
||||
@@ -36,9 +39,11 @@ credentials.json
|
||||
# =============================================================================
|
||||
# Build & Distribution
|
||||
# =============================================================================
|
||||
dist/
|
||||
build/
|
||||
out/
|
||||
# Neo vao goc — mau khong neo se nuot moi thu muc trung ten o moi do sau,
|
||||
# ke ca ma nguon. Da mac dung loi do voi secrets/ (xem khoi Credentials).
|
||||
/dist/
|
||||
/build/
|
||||
/out/
|
||||
.next/
|
||||
.nuxt/
|
||||
.output/
|
||||
@@ -73,7 +78,8 @@ desktop.ini
|
||||
# Logs & Debug
|
||||
# =============================================================================
|
||||
*.log
|
||||
logs/
|
||||
# Neo vao goc: infrastructure/logs/ la ma nguon, khong phai log chay may.
|
||||
/logs/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
"""adapters/ — Adapter riêng cho Qt (clock, thread, timer).
|
||||
|
||||
Kế hoạch gốc đặt tên thư mục này là ``platform/``. Không dùng được: chạy
|
||||
bất kỳ script nào từ thư mục gốc repo (``python tools/...``,
|
||||
``python scripts/...``) thì ``platform/`` **che khuất module ``platform``
|
||||
của thư viện chuẩn**, và ``import keyring`` chết ngay với
|
||||
``AttributeError: module 'platform' has no attribute 'system'``.
|
||||
Repo có 26 script chạy đúng kiểu đó.
|
||||
|
||||
Đổi tên là cách duy nhất chắc chắn — không thể bắt mọi người nhớ "đừng bao
|
||||
giờ chạy python từ thư mục gốc".
|
||||
"""
|
||||
@@ -0,0 +1 @@
|
||||
"""application/ — Điều phối use-case. KHÔNG import PySide6. Gọi domain + interface hạ tầng."""
|
||||
@@ -0,0 +1 @@
|
||||
"""Application conversations package: turn lifecycle orchestration and agent execution."""
|
||||
@@ -0,0 +1,322 @@
|
||||
"""The turn lifecycle, once, in pure Python (R04-T03).
|
||||
|
||||
Extracted from ``core/chat_agent.py::run_cowork``, whose 260-line body mixed the
|
||||
lifecycle (compose the prompt, call the model, dispatch tools, respect the step
|
||||
ceiling, tidy the sandbox) with the concrete machinery that does each of those
|
||||
things. The lifecycle is the part with rules worth testing — and the part that
|
||||
was untestable, because reaching it meant standing up a Qt widget and a worker
|
||||
thread.
|
||||
|
||||
Here it is a plain object driven through the seams in :mod:`turn_runtime`, so a
|
||||
test states a rule ("the guard runs before the model", "a rejected command never
|
||||
executes") in three lines. ``core/chat_agent.py`` keeps its signature and
|
||||
delegates, and the presentation layer keeps receiving the same events via the
|
||||
legacy codec, so nothing downstream had to change with it.
|
||||
|
||||
Behavioural contract: this is a faithful port, not an improvement pass. Where
|
||||
the original had a quirk (the step-ceiling note only merges into the answer when
|
||||
the last message is the assistant's), the quirk is preserved and commented —
|
||||
changing what a user sees belongs in its own change, not smuggled into a move.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from ...domain.agents.agent_event import (
|
||||
AssistantMessageCompletedEvent,
|
||||
ErrorEvent,
|
||||
OutputsAddedEvent,
|
||||
OutputsRemovedEvent,
|
||||
PlanStep,
|
||||
PlanUpdatedEvent,
|
||||
ReasoningChunkEvent,
|
||||
TextChunkEvent,
|
||||
ToolCallFinishedEvent,
|
||||
ToolCallStartedEvent,
|
||||
ToolOutputChunkEvent,
|
||||
)
|
||||
from ...domain.agents.agent_result import AgentResult
|
||||
from ...domain.agents.conversation_execution_request import ConversationExecutionRequest
|
||||
from .turn_runtime import (
|
||||
BUDGET_NOTE_TEMPLATE,
|
||||
GATED_TOOLS,
|
||||
PLAN_TOOL,
|
||||
REASONING_ONLY_NOTE,
|
||||
REJECTED_OUTPUT,
|
||||
AttachmentReader,
|
||||
CancelFn,
|
||||
CommandGuard,
|
||||
ContextCompactor,
|
||||
EventSink,
|
||||
ModelCallPort,
|
||||
PermissionRequest,
|
||||
PromptGuard,
|
||||
PromptPreparer,
|
||||
ToolRuntimePort,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("cowork_local.application.conversations")
|
||||
|
||||
|
||||
class ConversationApplicationService:
|
||||
"""Runs one :class:`ConversationExecutionRequest` to completion."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
model: ModelCallPort,
|
||||
tools: ToolRuntimePort,
|
||||
*,
|
||||
prepare_prompt: Optional[PromptPreparer] = None,
|
||||
prompt_guard: Optional[PromptGuard] = None,
|
||||
command_guard: Optional[CommandGuard] = None,
|
||||
compact: Optional[ContextCompactor] = None,
|
||||
permission_request: Optional[PermissionRequest] = None,
|
||||
attachment_reader: Optional[AttachmentReader] = None,
|
||||
) -> None:
|
||||
self._model = model
|
||||
self._tools = tools
|
||||
# Every hook is optional so the service degrades to a plain chat turn.
|
||||
# That is not only a test convenience: a headless caller legitimately has
|
||||
# no guards (``security_config=None`` today) and no permission dialog.
|
||||
self._prepare_prompt = prepare_prompt
|
||||
self._prompt_guard = prompt_guard
|
||||
self._command_guard = command_guard
|
||||
self._compact = compact
|
||||
self._permission_request = permission_request
|
||||
self._attachment_reader = attachment_reader
|
||||
|
||||
# -- public API ------------------------------------------------------ #
|
||||
def execute(self, request: ConversationExecutionRequest, sink: EventSink,
|
||||
cancel: Optional[CancelFn] = None,
|
||||
messages: Optional[List[Dict[str, Any]]] = None) -> AgentResult:
|
||||
"""Run the turn, streaming events to ``sink``, and report the outcome.
|
||||
|
||||
``messages``, when given, is a working list the caller already built —
|
||||
it MUST already end with this turn's user message, and the service
|
||||
appends into that very object instead of composing its own. The Cowork
|
||||
widget needs this: it hands out the same list to
|
||||
``_reattach_running_turn``, which replays the steps done so far while the
|
||||
worker is still appending, and to ``_finalize_turn``, which slices it by
|
||||
the pre-turn snapshot length. A private list would break both silently.
|
||||
Passing ``None`` (every headless caller) lets the service compose the
|
||||
list from the request, which is the mode the rest of this class assumes.
|
||||
|
||||
Raises whatever the runtime raises (a blocked prompt, a dead gateway):
|
||||
the caller already has a failure path for that — ``AgentWorker.failed``
|
||||
in the UI, the artifact writer in Schedule Task — and swallowing the
|
||||
exception here would silently turn a failed turn into an empty answer.
|
||||
An :class:`ErrorEvent` is emitted first so subscribers see the failure
|
||||
on the same stream as everything else.
|
||||
"""
|
||||
cancel = cancel or (lambda: False)
|
||||
|
||||
# -- pre-flight. Runs BEFORE the output snapshot, so a turn refused here
|
||||
# leaves the output folder completely untouched (tidying is not a
|
||||
# read-only operation — see ToolRuntimePort.finalize).
|
||||
try:
|
||||
# The caller's list is used by reference on purpose (see above); only
|
||||
# the self-composed path may build a fresh one.
|
||||
working = messages if messages is not None else self._compose_messages(request)
|
||||
tools = list(self._tools.specs(request.allowed_tools))
|
||||
if self._prepare_prompt is not None:
|
||||
self._prepare_prompt(working, tuple(getattr(t, "name", "") for t in tools))
|
||||
if request.enforce_rules and self._prompt_guard is not None:
|
||||
self._prompt_guard(working)
|
||||
except Exception as exc: # noqa: BLE001 — reported, then re-raised as-is
|
||||
sink(ErrorEvent(message=str(exc)))
|
||||
raise
|
||||
|
||||
before = self._tools.snapshot()
|
||||
steps_used = 0
|
||||
plan_steps: Tuple[PlanStep, ...] = ()
|
||||
completed_naturally = False
|
||||
try:
|
||||
for _ in range(request.effective_max_steps):
|
||||
if cancel():
|
||||
break
|
||||
# Auto-compress when nearing the model's context budget; a no-op
|
||||
# when off or when the conversation is still short.
|
||||
if self._compact is not None:
|
||||
self._compact(working, cancel)
|
||||
|
||||
assistant = self._model.call(
|
||||
working, tools,
|
||||
on_text=lambda piece: sink(TextChunkEvent(delta=piece)),
|
||||
on_reasoning=lambda piece: sink(ReasoningChunkEvent(delta=piece)),
|
||||
cancel=cancel,
|
||||
)
|
||||
working.append(assistant)
|
||||
steps_used += 1
|
||||
tool_calls = assistant.get("tool_calls") or []
|
||||
|
||||
if not tool_calls and not (assistant.get("content") or "").strip():
|
||||
# Written into the message, not just emitted, so the stored
|
||||
# conversation never ends on a blank assistant turn.
|
||||
assistant["content"] = REASONING_ONLY_NOTE
|
||||
sink(TextChunkEvent(delta=REASONING_ONLY_NOTE))
|
||||
sink(AssistantMessageCompletedEvent(content=assistant.get("content", "")))
|
||||
|
||||
if not tool_calls:
|
||||
completed_naturally = True
|
||||
break
|
||||
|
||||
for call in tool_calls:
|
||||
if cancel():
|
||||
break
|
||||
tool_message, steps = self._dispatch(request, call, sink, cancel)
|
||||
working.append(tool_message)
|
||||
if steps is not None:
|
||||
plan_steps = steps
|
||||
|
||||
if not completed_naturally and not cancel():
|
||||
self._announce_budget_exhausted(request, working, sink)
|
||||
except Exception as exc: # noqa: BLE001 — reported, then re-raised as-is
|
||||
sink(ErrorEvent(message=str(exc)))
|
||||
raise
|
||||
finally:
|
||||
# Always tidy: the sandbox and generator scripts must not survive a
|
||||
# turn that stopped abruptly. Runs on success, cancel and failure.
|
||||
self._finalize_outputs(before, sink, cancelled=cancel())
|
||||
|
||||
result = AgentResult(
|
||||
messages=working, steps_used=steps_used, cancelled=cancel(),
|
||||
budget_exhausted=not completed_naturally and not cancel(),
|
||||
plan_steps=plan_steps,
|
||||
)
|
||||
sink(result.to_turn_completed_event())
|
||||
return result
|
||||
|
||||
# -- internals ------------------------------------------------------- #
|
||||
def _compose_messages(self, request: ConversationExecutionRequest) -> List[Dict[str, Any]]:
|
||||
"""History snapshot plus this turn's user message.
|
||||
|
||||
The attachment text is read HERE rather than when the request was built,
|
||||
because extraction is slow enough to freeze the UI thread; the request
|
||||
deliberately carries paths only.
|
||||
"""
|
||||
body = request.prompt
|
||||
if self._attachment_reader is not None:
|
||||
body = self._attachment_reader(request.prompt, request.attachments)
|
||||
messages = [dict(m) for m in request.messages]
|
||||
messages.append({"role": "user", "content": request.user_content(body)})
|
||||
return messages
|
||||
|
||||
def _dispatch(self, request: ConversationExecutionRequest, call: Dict[str, Any],
|
||||
sink: EventSink, cancel: CancelFn
|
||||
) -> Tuple[Dict[str, Any], Optional[Tuple[PlanStep, ...]]]:
|
||||
"""Run one tool call.
|
||||
|
||||
Returns ``(tool_message, plan_steps)`` — the message to append to the
|
||||
conversation, and the new checklist when this call was the plan tool
|
||||
(``None`` otherwise, so the caller can tell "no change" from "empty
|
||||
plan").
|
||||
"""
|
||||
call_id = str(call.get("id", ""))
|
||||
name = str(call.get("name", ""))
|
||||
args = call.get("arguments") or {}
|
||||
|
||||
# The plan tool is invisible in the transcript: it updates the Plan panel
|
||||
# and nothing else, so it skips preview, guard and gate entirely.
|
||||
if name == PLAN_TOOL:
|
||||
outcome = self._tools.execute(name, args, on_output=None, cancel=cancel)
|
||||
steps = tuple(outcome.get("plan_steps") or ())
|
||||
sink(PlanUpdatedEvent(steps=steps))
|
||||
return self._tool_message(call_id, name, outcome.get("output", "")), steps
|
||||
|
||||
# Announce first: the user sees the code/command about to run before the
|
||||
# guard or the approval dialog interrupts them, which is the whole point
|
||||
# of showing the step CLI-style.
|
||||
preview = self._tools.preview(name, args)
|
||||
sink(ToolCallStartedEvent(call_id=call_id, name=name, arguments=dict(args),
|
||||
preview=preview))
|
||||
|
||||
if request.enforce_rules and self._command_guard is not None:
|
||||
self._command_guard(name, args)
|
||||
|
||||
if not self._approved(request, name, args, preview, sink, call_id):
|
||||
return self._tool_message(call_id, name, REJECTED_OUTPUT), None
|
||||
|
||||
outcome = self._tools.execute(
|
||||
name, args,
|
||||
on_output=lambda piece: sink(ToolOutputChunkEvent(
|
||||
call_id=call_id, name=name, delta=piece)),
|
||||
cancel=cancel,
|
||||
)
|
||||
sink(ToolCallFinishedEvent(
|
||||
call_id=call_id, name=name, ok=bool(outcome.get("ok", False)),
|
||||
output=str(outcome.get("output", "")), path=str(outcome.get("path", "") or ""),
|
||||
produced=outcome.get("produced") or (),
|
||||
))
|
||||
return self._tool_message(call_id, name, outcome.get("output", "")), None
|
||||
|
||||
def _approved(self, request: ConversationExecutionRequest, name: str,
|
||||
args: Dict[str, Any], preview: Any, sink: EventSink,
|
||||
call_id: str) -> bool:
|
||||
"""Whether this call may run.
|
||||
|
||||
Only command-shaped tools are gated, and only when the workspace asked
|
||||
to confirm them: file writes stay inside the turn's own sandbox, so
|
||||
prompting for those would be noise. A rejection is reported as a failed
|
||||
tool result — the model needs to read back that it was refused, or it
|
||||
will simply try the same call again.
|
||||
"""
|
||||
if not request.requires_permission_gate or name not in GATED_TOOLS:
|
||||
return True
|
||||
if self._permission_request is None:
|
||||
# Confirm mode with nobody to ask: refusing is the safe direction,
|
||||
# since auto-running is exactly what confirm mode exists to prevent.
|
||||
logger.warning("turn: confirm mode without a permission callback — refusing %r", name)
|
||||
approved = False
|
||||
else:
|
||||
approved = bool(self._permission_request({
|
||||
"name": name, "args": args,
|
||||
"preview": preview.to_dict() if preview is not None else {},
|
||||
}))
|
||||
if not approved:
|
||||
sink(ToolCallFinishedEvent(call_id=call_id, name=name, ok=False,
|
||||
output=REJECTED_OUTPUT))
|
||||
return approved
|
||||
|
||||
@staticmethod
|
||||
def _tool_message(call_id: str, name: str, output: Any) -> Dict[str, Any]:
|
||||
"""The canonical ``role: tool`` message the model reads back."""
|
||||
return {"role": "tool", "tool_call_id": call_id, "name": name,
|
||||
"content": str(output or "")}
|
||||
|
||||
@staticmethod
|
||||
def _announce_budget_exhausted(request: ConversationExecutionRequest,
|
||||
messages: List[Dict[str, Any]], sink: EventSink) -> None:
|
||||
"""Report being cut off by the step ceiling.
|
||||
|
||||
The note always reaches the transcript. It is merged into the stored
|
||||
answer only when the last message is the assistant's — which, when the
|
||||
ceiling is hit, it never is (the turn ends on a tool result). The branch
|
||||
is kept because it is what the current runtime does, and because it is
|
||||
the correct behaviour the day a caller ends the loop differently.
|
||||
"""
|
||||
note = BUDGET_NOTE_TEMPLATE.format(steps=request.effective_max_steps)
|
||||
sink(TextChunkEvent(delta=note))
|
||||
if messages and messages[-1].get("role") == "assistant":
|
||||
messages[-1]["content"] = (messages[-1].get("content") or "") + note
|
||||
|
||||
def _finalize_outputs(self, before: Any, sink: EventSink, cancelled: bool) -> None:
|
||||
"""Tidy the output folder and report what moved.
|
||||
|
||||
Failures are logged, never raised: this runs in a ``finally``, so an
|
||||
exception here would replace the turn's real error (or its success) with
|
||||
a housekeeping one.
|
||||
"""
|
||||
try:
|
||||
removed, added = self._tools.finalize(before, cancelled=cancelled)
|
||||
except Exception: # noqa: BLE001
|
||||
logger.exception("turn: tidying the output folder failed")
|
||||
return
|
||||
if removed:
|
||||
sink(OutputsRemovedEvent(paths=tuple(removed)))
|
||||
if added:
|
||||
sink(OutputsAddedEvent(paths=tuple(added)))
|
||||
|
||||
|
||||
__all__ = ["ConversationApplicationService"]
|
||||
@@ -0,0 +1,325 @@
|
||||
"""Wires :class:`ConversationApplicationService` to the existing runtime (R04-T03).
|
||||
|
||||
The service is written against the narrow seams in :mod:`turn_runtime` so it can
|
||||
be tested with plain fakes. This module supplies the real implementations — the
|
||||
provider call with its recovery pass, the tool/sandbox runtime, the security
|
||||
guards, context compaction — and is therefore the ONLY file in
|
||||
``application/conversations/`` that knows ``core/*`` exists. Same shape (and
|
||||
same reason) as ``application/model_routing/core_routing_adapter.py`` in R03.
|
||||
|
||||
Every ``core`` import is deferred into a method body: importing the tool runtime
|
||||
pulls in ``requests``, ``psutil`` and the sandbox stack, and code that merely
|
||||
*builds* a service must not pay for that.
|
||||
|
||||
Faithfulness notes — two places where this reproduces a quirk of the current
|
||||
runtime rather than the behaviour one would design fresh. Both are marked
|
||||
inline: the MS365 system-prompt paragraph keys off the CONFIGURED extra tools
|
||||
(not the advertised subset), and the ``tool_result`` path falls back to the
|
||||
call's own ``path`` argument resolved against the workdir.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict, List, Optional, Sequence, Tuple
|
||||
|
||||
from ...domain.agents.agent_event import PlanStep, ToolPreview
|
||||
from .conversation_application_service import ConversationApplicationService
|
||||
from .turn_runtime import PLAN_TOOL, EventSink
|
||||
|
||||
# Legacy emit: the dict-based callback every current caller already owns.
|
||||
LegacyEmit = Callable[[Dict[str, Any]], None]
|
||||
|
||||
|
||||
def legacy_event_sink(emit: LegacyEmit) -> EventSink:
|
||||
"""Adapt a typed :class:`EventSink` onto the legacy dict ``emit``.
|
||||
|
||||
This is what lets R04 land without touching the presentation layer: the
|
||||
service thinks in typed events, ``ui/chat_panel.py::_on_event`` keeps
|
||||
receiving exactly the dicts it already dispatches on. Deleted in R08 once
|
||||
the widget consumes events directly.
|
||||
"""
|
||||
return lambda event: emit(event.to_legacy_dict())
|
||||
|
||||
|
||||
class CoreModelCall:
|
||||
""":class:`ModelCallPort` over ``code_agent._call_provider_with_recovery``.
|
||||
|
||||
Not ``provider.chat`` directly: the recovery wrapper adds the one bounded
|
||||
retry that hides a dropped connection or a momentarily unreachable gateway,
|
||||
and losing it would be a visible regression on flaky corporate networks.
|
||||
"""
|
||||
|
||||
def __init__(self, provider: Any) -> None:
|
||||
self._provider = provider
|
||||
|
||||
def call(self, messages, tools, on_text=None, on_reasoning=None, cancel=None):
|
||||
from ...core.code_agent import _call_provider_with_recovery
|
||||
|
||||
return _call_provider_with_recovery(self._provider, messages, tools, on_text,
|
||||
cancel, on_reasoning)
|
||||
|
||||
|
||||
class CoreToolRuntime:
|
||||
""":class:`ToolRuntimePort` over ``core/tools.py`` + Cowork's file tools."""
|
||||
|
||||
def __init__(self, output_dir: Path, *, title: str = "",
|
||||
extra_tools: Optional[Sequence[Any]] = None, extra_executor=None,
|
||||
security_config: Any = None, agent_role: str = "") -> None:
|
||||
self._output_dir = Path(output_dir)
|
||||
self._title = title
|
||||
self._extra_tools = list(extra_tools or ())
|
||||
self._extra_names = {getattr(t, "name", "") for t in self._extra_tools}
|
||||
# The connector executor MCP/REST tools are routed to; None when the
|
||||
# turn has no connectors enabled.
|
||||
self._extra_executor = extra_executor
|
||||
self._security_config = security_config
|
||||
self._agent_role = agent_role
|
||||
self._ctx: Any = None # built on first use (see _tool_context)
|
||||
|
||||
# -- the configured extra tools, for the system-prompt hints ---------- #
|
||||
@property
|
||||
def extra_names(self) -> frozenset:
|
||||
return frozenset(self._extra_names)
|
||||
|
||||
def _tool_context(self):
|
||||
"""The sandboxed ``ToolContext`` every built-in tool call runs inside.
|
||||
|
||||
Built once per turn and cached: it carries the resource limits and the
|
||||
network policy, so re-deriving it mid-turn could let a Settings change
|
||||
take effect halfway through work already in flight.
|
||||
"""
|
||||
if self._ctx is None:
|
||||
from ...core import agent_security
|
||||
from ...core.tools import ToolContext
|
||||
|
||||
limits, block_network = agent_security.sandbox_settings(self._security_config)
|
||||
self._ctx = ToolContext(
|
||||
self._output_dir, flatten_writes=True, # keep every file in the Output root
|
||||
resource_limits=limits, block_network=block_network,
|
||||
allow_url_fetch=agent_security.url_fetch_allowed(self._security_config),
|
||||
jira=(self._security_config.data.get("jira") if self._security_config else None),
|
||||
)
|
||||
return self._ctx
|
||||
|
||||
# -- ToolRuntimePort -------------------------------------------------- #
|
||||
def specs(self, allowed_tools: Optional[Sequence[str]] = None) -> List[Any]:
|
||||
"""Advertised tools: Cowork's own two, the enabled built-ins, then MCP.
|
||||
|
||||
``allowed_tools`` restricts the list so a read-only step literally cannot
|
||||
write. ``update_plan`` and the connector tools always survive the filter:
|
||||
the plan tool has no side effects, and connectors are opted into
|
||||
explicitly rather than governed by the built-in capability scope.
|
||||
"""
|
||||
from ...core.chat_agent import SAVE_FILE_SPEC
|
||||
from ...core.plan import UPDATE_PLAN_SPEC
|
||||
from ...core.tools import enabled_tool_specs
|
||||
|
||||
specs = ([SAVE_FILE_SPEC, UPDATE_PLAN_SPEC]
|
||||
+ list(enabled_tool_specs(self._security_config))
|
||||
+ self._extra_tools)
|
||||
if allowed_tools is None:
|
||||
return specs
|
||||
allow = set(allowed_tools) | {PLAN_TOOL} | self._extra_names
|
||||
return [t for t in specs if getattr(t, "name", "") in allow]
|
||||
|
||||
def preview(self, name: str, args: Dict[str, Any]) -> Optional[ToolPreview]:
|
||||
"""What the user sees before the call runs."""
|
||||
# A connector call has no local diff to show, so it renders as the plain
|
||||
# argument dump the runtime already used.
|
||||
if name in self._extra_names:
|
||||
return ToolPreview(kind="info", title=name, text=str(args))
|
||||
if name == "save_file":
|
||||
return self._save_file_preview(args)
|
||||
from ...core.tools import describe_action
|
||||
|
||||
raw = describe_action(self._tool_context(), name, args)
|
||||
return ToolPreview.from_dict(raw)
|
||||
|
||||
def _save_file_preview(self, args: Dict[str, Any]) -> ToolPreview:
|
||||
"""A before/after diff for the file the agent is about to write.
|
||||
|
||||
A brand-new file renders all-green (before is empty); an overwrite shows
|
||||
the real change, so saving a file reads like editing one.
|
||||
"""
|
||||
import difflib
|
||||
|
||||
from ...core.chat_agent import _structure_summary, _titled_filename
|
||||
|
||||
fname = _titled_filename(self._title, args.get("filename", "output.txt"))
|
||||
content = str(args.get("content", ""))
|
||||
summary = _structure_summary(fname, content)
|
||||
old = ""
|
||||
existing = self._output_dir / fname
|
||||
if existing.exists():
|
||||
try:
|
||||
old = existing.read_text(encoding="utf-8", errors="replace")
|
||||
except OSError:
|
||||
pass # unreadable existing file: show it as a fresh write
|
||||
diff = "".join(difflib.unified_diff(
|
||||
old.splitlines(keepends=True), content.splitlines(keepends=True),
|
||||
fromfile=f"a/{fname}", tofile=f"b/{fname}",
|
||||
)) or content[:4000]
|
||||
return ToolPreview(kind="diff", title=f"Save {fname}",
|
||||
text=f"{summary}\n\n{diff[:4000]}")
|
||||
|
||||
def execute(self, name: str, args: Dict[str, Any], on_output=None,
|
||||
cancel=None) -> Dict[str, Any]:
|
||||
"""Run one tool call and return the runtime's result mapping."""
|
||||
if name == PLAN_TOOL:
|
||||
return self._execute_plan(args)
|
||||
if name in self._extra_names and self._extra_executor is not None:
|
||||
# Connector results carry no local file, so no path/produced keys —
|
||||
# matching what the runtime reports for an MCP call today.
|
||||
result = self._extra_executor(name, args) or {}
|
||||
return {"ok": bool(result.get("ok", False)), "output": result.get("output", "")}
|
||||
if name == "save_file":
|
||||
from ...core.chat_agent import _do_save_file
|
||||
|
||||
return dict(_do_save_file(self._output_dir, self._title, args))
|
||||
|
||||
from ...core.tools import execute_tool
|
||||
|
||||
ctx = self._tool_context()
|
||||
result = dict(execute_tool(ctx, name, args, cancel=cancel, on_output=on_output,
|
||||
agent_role=self._agent_role))
|
||||
# Quirk preserved: a tool that wrote the file named in its OWN arguments
|
||||
# (write_file/edit_file) does not report a path, so the runtime derives
|
||||
# one from the argument. Dropping this would empty the Output list.
|
||||
if not result.get("path") and isinstance(args, dict) and args.get("path"):
|
||||
result["path"] = str(ctx.workdir / str(args["path"]))
|
||||
return result
|
||||
|
||||
def _execute_plan(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Apply an ``update_plan`` call: validate the steps and audit them.
|
||||
|
||||
Produces no file and no chat bubble; the service turns the returned
|
||||
steps into a single plan event.
|
||||
"""
|
||||
from ...core import agent_roles, audit_log
|
||||
from ...core.plan import normalize_plan_steps
|
||||
|
||||
steps = normalize_plan_steps(args.get("steps"))
|
||||
audit_log.record("tool_call", PLAN_TOOL, True, f"{len(steps)} step(s)",
|
||||
agent_role=agent_roles.PLANNER)
|
||||
return {"ok": True, "output": "Plan updated.",
|
||||
"plan_steps": [PlanStep(title=s["title"], status=s["status"]) for s in steps]}
|
||||
|
||||
def snapshot(self) -> Any:
|
||||
from ...core.tools import _snapshot
|
||||
|
||||
return _snapshot(self._output_dir)
|
||||
|
||||
def finalize(self, before: Any, cancelled: bool = False
|
||||
) -> Tuple[List[str], List[str]]:
|
||||
"""Drop the scratch sandbox and flatten deliverables into the root.
|
||||
|
||||
Returns ``(gone, arrived)``: a file that MOVED counts as both, because
|
||||
the Output list keys entries by path and must drop the old one.
|
||||
"""
|
||||
from ...core.chat_agent import _cleanup_cowork_intermediates
|
||||
|
||||
removed, moved = _cleanup_cowork_intermediates(self._output_dir, before,
|
||||
cancelled=cancelled)
|
||||
gone = list(removed) + [old for old, _new in moved]
|
||||
arrived = [new for _old, new in moved]
|
||||
return gone, arrived
|
||||
|
||||
|
||||
def build_cowork_conversation_service(
|
||||
provider: Any,
|
||||
output_dir: Path,
|
||||
emit: LegacyEmit,
|
||||
*,
|
||||
title: str = "",
|
||||
project_context: str = "",
|
||||
extra_tools: Optional[Sequence[Any]] = None,
|
||||
extra_executor=None,
|
||||
security_config: Any = None,
|
||||
gate: Any = None,
|
||||
agent_role: str = "",
|
||||
) -> ConversationApplicationService:
|
||||
"""A service wired to the real runtime, ready to execute a Cowork turn.
|
||||
|
||||
``emit`` is the legacy dict callback: the guards and the compactor publish
|
||||
their own notices through it directly (exactly as they do now), while the
|
||||
service's typed events reach it via :func:`legacy_event_sink`.
|
||||
|
||||
``gate`` present means the workspace asked to confirm commands; pass the
|
||||
request with ``gate_mode="confirm"`` so the two agree. A gate of ``None``
|
||||
keeps the pre-existing auto-run behaviour.
|
||||
"""
|
||||
from ...core import agent_roles
|
||||
|
||||
tools = CoreToolRuntime(
|
||||
output_dir, title=title, extra_tools=extra_tools, extra_executor=extra_executor,
|
||||
security_config=security_config, agent_role=agent_role or agent_roles.COWORK,
|
||||
)
|
||||
|
||||
def prepare_prompt(messages: List[Dict[str, Any]], advertised: Tuple[str, ...]) -> None:
|
||||
"""Insert the system prompt, then fold in skills, rules and project text.
|
||||
|
||||
``advertised`` is unused on purpose: the runtime decides the MS365
|
||||
paragraph from the CONFIGURED connector tools, not from the subset a
|
||||
capability scope left advertised. Changing that changes the prompt the
|
||||
model sees, so it stays as-is here and belongs to R05's tool-policy work.
|
||||
"""
|
||||
from ...core.chat_agent import (
|
||||
COWORK_TOOL_PROMPT,
|
||||
OPENDATALOADER_PDF_PROMPT,
|
||||
_apply_project_context,
|
||||
_apply_security_rules,
|
||||
_apply_skills,
|
||||
)
|
||||
from ...core.deps import _can_pip
|
||||
from ...core.java_runtime import find_java
|
||||
from ...core.security_rules import load_rules
|
||||
from ...core.skills import active_skills_text
|
||||
|
||||
if not messages or messages[0].get("role") != "system":
|
||||
system = COWORK_TOOL_PROMPT
|
||||
if any(n.startswith("ms365_") for n in tools.extra_names):
|
||||
system += ("\nThe user has signed in to Microsoft 365 and enabled some ms365__* "
|
||||
"tools (Outlook / Teams / OneDrive / SharePoint / meeting transcripts, "
|
||||
"via the built-in MS365 MCP server). Use them whenever the request "
|
||||
"involves that data — don't say you can't access it.")
|
||||
if find_java() is not None and _can_pip():
|
||||
# Only advertise the Java-backed PDF extractor when BOTH the JVM
|
||||
# and pip are available, so the agent is never steered into a
|
||||
# command that cannot work on this machine.
|
||||
system += "\n\n" + OPENDATALOADER_PDF_PROMPT
|
||||
messages.insert(0, {"role": "system", "content": system})
|
||||
_apply_skills(messages, active_skills_text())
|
||||
_apply_security_rules(messages, load_rules())
|
||||
_apply_project_context(messages, project_context)
|
||||
|
||||
def prompt_guard(messages: List[Dict[str, Any]]) -> None:
|
||||
from ...core import agent_security
|
||||
|
||||
agent_security.enforce_prompt(provider, messages, security_config, emit)
|
||||
|
||||
def command_guard(name: str, args: Dict[str, Any]) -> None:
|
||||
from ...core import agent_security
|
||||
|
||||
agent_security.enforce_command(provider, name, args, security_config, emit)
|
||||
|
||||
def compact(messages: List[Dict[str, Any]], cancel) -> None:
|
||||
from ...core import context_budget
|
||||
|
||||
context_budget.maybe_compact(provider, messages, security_config,
|
||||
emit=emit, cancel=cancel)
|
||||
|
||||
return ConversationApplicationService(
|
||||
CoreModelCall(provider), tools,
|
||||
prepare_prompt=prepare_prompt,
|
||||
prompt_guard=prompt_guard,
|
||||
command_guard=command_guard,
|
||||
compact=compact,
|
||||
permission_request=(gate.request if gate is not None else None),
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
"LegacyEmit", "legacy_event_sink", "CoreModelCall", "CoreToolRuntime",
|
||||
"build_cowork_conversation_service",
|
||||
]
|
||||
@@ -0,0 +1,77 @@
|
||||
"""Turn the Cowork widget's captured state into a request (R04-T04).
|
||||
|
||||
``ui/cowork_tab.py::build_job`` reads a dozen values off the widget on the UI
|
||||
thread and has to translate three of them before a turn can run: which message
|
||||
is this turn's prompt, which messages are its history, and whether the workspace
|
||||
wants commands confirmed. Those rules lived inline in the widget, where no test
|
||||
could reach them — and each fails silently when wrong (a duplicated user message,
|
||||
or a command that quietly stops asking for approval).
|
||||
|
||||
They live here instead, as the mapping step the migration map assigns to the
|
||||
application layer. The widget keeps only what is genuinely widget-specific:
|
||||
reading its own state and building the provider.
|
||||
|
||||
Layer rules (``docs/architecture/ADR-001-layered-architecture.md``): pure Python.
|
||||
Everything arrives as a plain value, so this module never sees a widget.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, Optional, Sequence
|
||||
|
||||
from ...domain.agents.conversation_execution_request import ConversationExecutionRequest
|
||||
|
||||
|
||||
def build_cowork_turn_request(
|
||||
*,
|
||||
turn_id: str,
|
||||
session_id: str,
|
||||
messages: Sequence[Dict[str, Any]],
|
||||
surface: str = "cowork",
|
||||
project_id: str = "",
|
||||
title: str = "",
|
||||
provider_id: str = "",
|
||||
model: str = "",
|
||||
instructions: str = "",
|
||||
output_dir: Optional[Any] = None,
|
||||
home_output_root: Optional[Any] = None,
|
||||
confirm_commands: bool = False,
|
||||
agent_role: str = "cowork",
|
||||
) -> ConversationExecutionRequest:
|
||||
"""Build one Cowork turn's immutable request.
|
||||
|
||||
``messages`` is the widget's working list, which ALREADY ends with this
|
||||
turn's user message (the chat panel composes it — prefix, attachments,
|
||||
session notes — before the job starts). So the prompt is that last message
|
||||
and the history is everything before it. The request records both; the
|
||||
service is handed the same working list and appends into it.
|
||||
|
||||
Keyword-only on purpose: a dozen positional strings in a call site is exactly
|
||||
how a title ends up in the project-id slot.
|
||||
"""
|
||||
history = list(messages or ())
|
||||
# ``pop`` rather than ``[-1]``/``[:-1]`` so the empty-list case needs no
|
||||
# special branch: a turn with nothing in it yields an empty prompt instead of
|
||||
# raising IndexError deep inside a worker thread.
|
||||
last = history.pop() if history else {}
|
||||
return ConversationExecutionRequest(
|
||||
turn_id=turn_id,
|
||||
session_id=session_id,
|
||||
surface=surface,
|
||||
project_id=project_id,
|
||||
title=title,
|
||||
prompt=str(last.get("content") or ""),
|
||||
messages=history,
|
||||
provider_id=provider_id,
|
||||
model=model,
|
||||
project_context=instructions,
|
||||
output_dir=output_dir,
|
||||
home_output_root=home_output_root,
|
||||
# The workspace's Auto-run override (or the global setting) decides
|
||||
# whether run_command/install_package must be approved first.
|
||||
gate_mode="confirm" if confirm_commands else "auto",
|
||||
agent_role=agent_role,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["build_cowork_turn_request"]
|
||||
@@ -0,0 +1,177 @@
|
||||
"""The seams :mod:`conversation_application_service` runs a turn through (R04-T03).
|
||||
|
||||
Two Protocols and six callables — chosen deliberately, not by reflex. The
|
||||
refactor plan forbids giving every class an interface, so a contract exists here
|
||||
only where there is both a real ``core/*`` implementation AND a test double:
|
||||
|
||||
* :class:`ModelCallPort` — one provider round-trip *including* the app's
|
||||
existing context-overflow recovery, which is why the raw ``Provider.chat``
|
||||
signature is not enough.
|
||||
* :class:`ToolRuntimePort` — the tool + output-folder runtime, kept as one
|
||||
cohesive object because every method operates on the same sandbox.
|
||||
|
||||
Everything else is a single function, so it is expressed as a callable type
|
||||
rather than a class with one method (the same choice R03 made for
|
||||
``ConfirmationCallback``). All of them are optional: a service built with none
|
||||
of them still runs a plain chat turn, which is what keeps the unit tests short.
|
||||
|
||||
Layer rules (``docs/architecture/ADR-001-layered-architecture.md``): application
|
||||
layer — pure Python. Nothing here imports PySide6, ``core.*``, ``providers.*``
|
||||
or ``ui.*``; the concrete wiring lives in :mod:`core_runtime_adapter`.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import (
|
||||
Any,
|
||||
Callable,
|
||||
Dict,
|
||||
List,
|
||||
Optional,
|
||||
Protocol,
|
||||
Sequence,
|
||||
Tuple,
|
||||
runtime_checkable,
|
||||
)
|
||||
|
||||
from ...domain.agents.agent_event import AgentEvent, ToolPreview
|
||||
|
||||
# The plan tool is special-cased by the loop: it drives the Plan panel and
|
||||
# produces no chat bubble and no file. Named here so the check is not a bare
|
||||
# string literal in the middle of the dispatch.
|
||||
PLAN_TOOL = "update_plan"
|
||||
|
||||
# Tools that need approval before they run when the workspace is in confirm
|
||||
# mode. R05 replaces this tuple with a real ``ToolPolicyGateway`` keyed on
|
||||
# ToolCapability; until then it mirrors exactly what the runtime gates today.
|
||||
GATED_TOOLS = ("run_command", "install_package")
|
||||
|
||||
# Shown when the user (or the workspace policy) rejects a proposed command. The
|
||||
# exact string also becomes the tool message the model reads back, so it must
|
||||
# stay stable.
|
||||
REJECTED_OUTPUT = "Rejected by user."
|
||||
|
||||
# A reasoning model can answer with thinking only. The note is written into the
|
||||
# assistant message itself, not merely emitted, so an unattended run does not
|
||||
# read back an empty answer and report "(no output)".
|
||||
REASONING_ONLY_NOTE = "*(model returned only its reasoning — try rephrasing)*"
|
||||
|
||||
# Emitted when the turn is stopped by its own safety ceiling rather than by the
|
||||
# model finishing. Never silent: being cut off looks exactly like being done.
|
||||
BUDGET_NOTE_TEMPLATE = (
|
||||
"\n\n⚠️ Reached the {steps}-step safety limit before the task signalled "
|
||||
"completion — stopping here. Re-run to continue if more work remains."
|
||||
)
|
||||
|
||||
|
||||
def combine_instructions(*blocks: Optional[str]) -> str:
|
||||
"""Join the standing-instruction blocks of a turn, skipping the absent ones.
|
||||
|
||||
A turn's instructions arrive as several independent blocks — the project's
|
||||
shared context, an Admin agent's persona, a skill's rules, the
|
||||
"this runs unattended" reminder — and each caller was joining them inline
|
||||
with its own ``f"{a}\\n\\n{b}" if a else b`` expression. Two call sites now
|
||||
need the same rule (the Cowork widget in R04-T04 and the task runner in
|
||||
R04-T05), which is the point at which it stops being an expression.
|
||||
|
||||
Whitespace-only blocks count as absent: they would otherwise open the system
|
||||
prompt with a stray blank line.
|
||||
"""
|
||||
return "\n\n".join(b.strip() for b in blocks if b and b.strip())
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Callables.
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Receives every typed event the turn produces. The caller decides what that
|
||||
# means — render it, forward it as a legacy dict, autosave on it.
|
||||
EventSink = Callable[[AgentEvent], None]
|
||||
|
||||
# True once the user has asked to stop. Polled between steps and between tool
|
||||
# calls, the same cadence the current runtime uses.
|
||||
CancelFn = Callable[[], bool]
|
||||
|
||||
# ``(prompt, attachment_paths) -> body``. Runs on the worker thread because
|
||||
# extracting a .docx may pip-install a parser or call LibreOffice.
|
||||
AttachmentReader = Callable[[str, Tuple[str, ...]], str]
|
||||
|
||||
# ``(messages, advertised_tool_names) -> None`` — inserts the system prompt and
|
||||
# folds in skills, security rules and project instructions, in place. It needs
|
||||
# the tool names because the system prompt gains an MS365 paragraph only when
|
||||
# ms365 tools are actually present.
|
||||
PromptPreparer = Callable[[List[Dict[str, Any]], Tuple[str, ...]], None]
|
||||
|
||||
# Reviews the assembled request; raises to refuse the turn outright.
|
||||
PromptGuard = Callable[[List[Dict[str, Any]]], None]
|
||||
|
||||
# Reviews one proposed tool call; raises to refuse it.
|
||||
CommandGuard = Callable[[str, Dict[str, Any]], None]
|
||||
|
||||
# ``(messages, cancel) -> None``. Summarises old turns in place when the
|
||||
# conversation nears the model's context budget; a no-op when compaction is off
|
||||
# or the conversation is short. It takes the cancel signal because compacting
|
||||
# calls the model itself, so Stop has to reach it too.
|
||||
ContextCompactor = Callable[[List[Dict[str, Any]], "CancelFn"], None]
|
||||
|
||||
# ``(action) -> approved``. Blocks the worker thread while a human decides.
|
||||
PermissionRequest = Callable[[Dict[str, Any]], bool]
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Ports.
|
||||
# --------------------------------------------------------------------------- #
|
||||
@runtime_checkable
|
||||
class ModelCallPort(Protocol):
|
||||
"""One call to the model, with the app's retry/recovery behaviour applied."""
|
||||
|
||||
def call(self, messages: List[Dict[str, Any]], tools: Sequence[Any],
|
||||
on_text: Optional[Callable[[str], None]] = None,
|
||||
on_reasoning: Optional[Callable[[str], None]] = None,
|
||||
cancel: Optional[CancelFn] = None) -> Dict[str, Any]:
|
||||
"""Return the canonical assistant message (content plus tool calls)."""
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ToolRuntimePort(Protocol):
|
||||
"""The tools a turn may call, and the folder its files land in."""
|
||||
|
||||
def specs(self, allowed_tools: Optional[Sequence[str]] = None) -> Sequence[Any]:
|
||||
"""Tool specs to advertise to the model, already filtered.
|
||||
|
||||
Returns opaque objects (the provider layer's ``ToolSpec``); the service
|
||||
only ever reads ``.name`` off them, which is what keeps this layer free
|
||||
of a provider import.
|
||||
"""
|
||||
|
||||
def preview(self, name: str, args: Dict[str, Any]) -> Optional[ToolPreview]:
|
||||
"""Human-readable description of a call that is about to run."""
|
||||
|
||||
def execute(self, name: str, args: Dict[str, Any],
|
||||
on_output: Optional[Callable[[str], None]] = None,
|
||||
cancel: Optional[CancelFn] = None) -> Dict[str, Any]:
|
||||
"""Run one tool call.
|
||||
|
||||
Returns the runtime's own result mapping: ``ok``, ``output``, optionally
|
||||
``path``/``produced`` for files it created, and ``plan_steps`` for the
|
||||
plan tool.
|
||||
"""
|
||||
|
||||
def snapshot(self) -> Any:
|
||||
"""Opaque record of the output folder before the turn started."""
|
||||
|
||||
def finalize(self, before: Any, cancelled: bool = False
|
||||
) -> Tuple[Sequence[str], Sequence[str]]:
|
||||
"""Tidy the output folder; return ``(removed_paths, added_paths)``.
|
||||
|
||||
Not read-only — it deletes the scratch sandbox and flattens sub-folders —
|
||||
so the service only calls it for a turn that actually started.
|
||||
"""
|
||||
|
||||
|
||||
__all__ = [
|
||||
"PLAN_TOOL", "GATED_TOOLS", "REJECTED_OUTPUT", "REASONING_ONLY_NOTE",
|
||||
"BUDGET_NOTE_TEMPLATE", "combine_instructions",
|
||||
"EventSink", "CancelFn", "AttachmentReader", "PromptPreparer", "PromptGuard",
|
||||
"CommandGuard", "ContextCompactor", "PermissionRequest",
|
||||
"ModelCallPort", "ToolRuntimePort",
|
||||
]
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Application model routing package: model route decisions and multi-provider balancing.
|
||||
|
||||
Public surface (R03-T03 — the single routing entry point every chat surface uses):
|
||||
|
||||
* :class:`RoutingApplicationService` — decides one turn's provider/model.
|
||||
* :class:`RoutingRequest` / :class:`RoutingOutcome` — the immutable DTOs in and out.
|
||||
* :class:`RoutingMode` — Off / Auto / Manual / Fallback.
|
||||
* :func:`build_routing_application_service` — wires the service to a live
|
||||
``AppContext`` (engine + per-workspace mode + confirm timeout).
|
||||
|
||||
Typical call site (see ``ui/chat_panel.py::_apply_routing``)::
|
||||
|
||||
service = build_routing_application_service(self.ctx)
|
||||
outcome = service.resolve(
|
||||
RoutingRequest(surface="cowork", prompt=text,
|
||||
current_provider=provider, current_model=model),
|
||||
confirm=lambda decision, timeout: confirm_switch(self, decision, timeout),
|
||||
)
|
||||
|
||||
Only ``core_routing_adapter`` touches ``core/routing``; the service and the DTOs
|
||||
stay pure Python so the whole rule set is testable without Qt or the engine.
|
||||
"""
|
||||
|
||||
from .core_routing_adapter import (
|
||||
AppContextModeResolver,
|
||||
CoreRoutingEngine,
|
||||
build_routing_application_service,
|
||||
)
|
||||
from .routing_application_service import (
|
||||
ConfirmationCallback,
|
||||
ModeResolver,
|
||||
RoutingApplicationService,
|
||||
RoutingDecisionPort,
|
||||
)
|
||||
from .routing_models import (
|
||||
RouteEvaluation,
|
||||
RoutingMode,
|
||||
RoutingOutcome,
|
||||
RoutingRequest,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"AppContextModeResolver",
|
||||
"ConfirmationCallback",
|
||||
"CoreRoutingEngine",
|
||||
"ModeResolver",
|
||||
"RouteEvaluation",
|
||||
"RoutingApplicationService",
|
||||
"RoutingDecisionPort",
|
||||
"RoutingMode",
|
||||
"RoutingOutcome",
|
||||
"RoutingRequest",
|
||||
"build_routing_application_service",
|
||||
]
|
||||
@@ -0,0 +1,169 @@
|
||||
"""Adapters that plug the existing routing engine into the application service.
|
||||
|
||||
:mod:`routing_application_service` is written against two narrow ports so it can
|
||||
be unit-tested with plain fakes. This module supplies the real implementations —
|
||||
the assessment/scoring engine in ``core/routing`` and the per-workspace mode
|
||||
lookup on ``AppContext`` — and is therefore the ONLY file in
|
||||
``application/model_routing/`` that knows those concrete types exist.
|
||||
|
||||
All engine imports are deferred into method bodies. Importing the routing stack
|
||||
pulls in Pydantic models and the on-disk assessment store, and the UI must be
|
||||
able to import this module during startup without paying that cost (the same
|
||||
lazy-wiring reason ``state.py::AppContext.routing`` gives).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
|
||||
from .routing_application_service import RoutingApplicationService
|
||||
from .routing_models import RouteEvaluation, RoutingMode, RoutingRequest
|
||||
|
||||
logger = logging.getLogger("cowork_local.application.model_routing")
|
||||
|
||||
|
||||
class CoreRoutingEngine:
|
||||
""":class:`RoutingDecisionPort` backed by ``core/routing/service.py``.
|
||||
|
||||
Translates in both directions: application DTOs in, and the engine's
|
||||
``RouteResult``/``SwitchDecision``/``TaskType`` flattened back out into a
|
||||
:class:`RouteEvaluation`, so no ``core.routing`` type ever escapes into the
|
||||
application service or the UI call sites.
|
||||
"""
|
||||
|
||||
def __init__(self, routing_service: Any) -> None:
|
||||
self._routing_service = routing_service
|
||||
|
||||
def evaluate(self, request: RoutingRequest, mode: RoutingMode) -> RouteEvaluation:
|
||||
"""Rank candidates for this turn and report the engine's verdict."""
|
||||
from ...core.routing.models import TaskType, candidate_key
|
||||
|
||||
result = self._routing_service.route(
|
||||
request.surface,
|
||||
request.prompt,
|
||||
request.current_provider,
|
||||
request.current_model,
|
||||
# The engine only knows off/auto/manual; FALLBACK was already mapped
|
||||
# to AUTO upstream so the value handed over here is always valid.
|
||||
mode_override=mode.value,
|
||||
required_capabilities=list(request.required_capabilities) or None,
|
||||
task_type=self._parse_task_type(request.task_type, TaskType),
|
||||
)
|
||||
|
||||
decision = result.decision
|
||||
target = result.target() # (provider, model_id) or None
|
||||
current_key = (
|
||||
candidate_key(request.current_provider, request.current_model)
|
||||
if request.current_model
|
||||
else ""
|
||||
)
|
||||
return RouteEvaluation(
|
||||
task_type=self._task_type_value(result.task_type),
|
||||
should_switch=bool(result.should_switch),
|
||||
target_provider=target[0] if target else None,
|
||||
target_model=target[1] if target else None,
|
||||
score_gain=float(getattr(decision, "score_gain", 0.0) or 0.0),
|
||||
reason=str(getattr(decision, "reason", "") or ""),
|
||||
current_is_usable=self._current_is_usable(result, current_key),
|
||||
decision=decision,
|
||||
)
|
||||
|
||||
# -- translation helpers --------------------------------------------- #
|
||||
@staticmethod
|
||||
def _parse_task_type(raw: Optional[str], task_type_enum) -> Optional[Any]:
|
||||
"""Coerce a task-type string to the engine's enum.
|
||||
|
||||
``None`` (the common case) means "let the engine classify the prompt".
|
||||
An unrecognised string is also downgraded to ``None`` rather than
|
||||
raising, so a stale value in a saved workspace cannot break a turn.
|
||||
"""
|
||||
if raw is None:
|
||||
return None
|
||||
if isinstance(raw, task_type_enum):
|
||||
return raw
|
||||
try:
|
||||
return task_type_enum(str(raw).strip().lower())
|
||||
except ValueError:
|
||||
logger.warning("routing: unknown task type %r — classifying from the prompt", raw)
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def _task_type_value(task_type: Any) -> str:
|
||||
"""The plain string form of the engine's task type enum."""
|
||||
return str(getattr(task_type, "value", task_type) or "")
|
||||
|
||||
@staticmethod
|
||||
def _current_is_usable(result: Any, current_key: str) -> bool:
|
||||
"""Whether the currently selected model can still serve this task.
|
||||
|
||||
This is the signal FALLBACK mode acts on. A model is usable when the
|
||||
ranking scored it above zero; ``rank_models`` already drops candidates
|
||||
that are unavailable, lack a probe for this task type, or failed their
|
||||
last probe, so "absent from the ranking" is precisely "cannot serve it".
|
||||
|
||||
With no ranking (routing off, or the engine's internal error path) or no
|
||||
current model, we answer True: absence of evidence must not trigger a
|
||||
surprise switch in a mode whose whole promise is not to surprise.
|
||||
"""
|
||||
ranking = getattr(result, "ranking", None)
|
||||
if ranking is None or not current_key:
|
||||
return True
|
||||
try:
|
||||
return float(ranking.score_of(current_key)) > 0.0
|
||||
except Exception: # noqa: BLE001 — defensive: never fail a turn on telemetry-ish data
|
||||
logger.debug("routing: could not score current model %r", current_key, exc_info=True)
|
||||
return True
|
||||
|
||||
|
||||
class AppContextModeResolver:
|
||||
""":class:`ModeResolver` backed by the active workspace's settings.
|
||||
|
||||
Reads through ``AppContext.project_routing_mode``, which already layers the
|
||||
workspace override on top of the global default — so per-workspace routing
|
||||
modes keep working unchanged now that the mode lookup moved out of the
|
||||
widgets.
|
||||
"""
|
||||
|
||||
def __init__(self, ctx: Any) -> None:
|
||||
self._ctx = ctx
|
||||
|
||||
def mode_for(self, surface: str) -> RoutingMode:
|
||||
"""Effective mode for ``surface`` in the active workspace."""
|
||||
return RoutingMode.parse(self._ctx.project_routing_mode(surface))
|
||||
|
||||
|
||||
def build_routing_application_service(ctx: Any) -> RoutingApplicationService:
|
||||
"""The shared :class:`RoutingApplicationService` for this app context.
|
||||
|
||||
Cached on the context (like ``AppContext.routing()`` caches the engine) so
|
||||
every surface talks to the same instance and a future stateful addition —
|
||||
per-surface cool-down, switch history — is shared rather than duplicated per
|
||||
widget. Falls back to a fresh instance if the context refuses attribute
|
||||
assignment, which keeps tests using lightweight stand-ins working.
|
||||
"""
|
||||
cached = getattr(ctx, "_routing_app_service", None)
|
||||
if cached is not None:
|
||||
return cached
|
||||
|
||||
service = RoutingApplicationService(
|
||||
CoreRoutingEngine(ctx.routing()),
|
||||
AppContextModeResolver(ctx),
|
||||
# Read at call time: the user can change the confirm timeout in Settings
|
||||
# between two turns and the next Manual dialog should honour it.
|
||||
confirm_timeout_sec=lambda: float(
|
||||
(ctx.config.routing or {}).get("confirm_timeout_sec", 60) or 60
|
||||
),
|
||||
)
|
||||
try:
|
||||
ctx._routing_app_service = service
|
||||
except Exception: # noqa: BLE001 — read-only/slotted stand-ins stay supported
|
||||
logger.debug("routing: could not cache the application service on the context", exc_info=True)
|
||||
return service
|
||||
|
||||
|
||||
__all__ = [
|
||||
"AppContextModeResolver",
|
||||
"CoreRoutingEngine",
|
||||
"build_routing_application_service",
|
||||
]
|
||||
@@ -0,0 +1,236 @@
|
||||
"""The one place that decides how a turn is routed (R03-T03).
|
||||
|
||||
Before this service, ``ui/chat_panel.py#L638``, ``ui/co4e_tab.py`` and
|
||||
``ui/folder_tab.py`` each carried their own copy of the same eight-step dance:
|
||||
clear last turn's override → read the surface's mode → bail on "off" → call the
|
||||
routing engine → check ``should_switch`` → resolve the target → show the Manual
|
||||
confirm dialog → publish the override and a status line. Three copies meant
|
||||
three chances to drift, and none of them could be tested without a Qt widget.
|
||||
|
||||
The dance now lives here, once, in pure Python:
|
||||
|
||||
* the routing engine is reached through :class:`RoutingDecisionPort`;
|
||||
* the surface's Off/Auto/Manual/Fallback mode through :class:`ModeResolver`;
|
||||
* the Manual-mode confirmation through a ``confirm`` callback supplied per call,
|
||||
so the Qt dialog stays in the presentation layer where it belongs.
|
||||
|
||||
Every failure path degrades to "keep the current model": a routing problem must
|
||||
never be the reason a user cannot send a message.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any, Callable, Optional, Protocol, runtime_checkable
|
||||
|
||||
from .routing_models import (
|
||||
RouteEvaluation,
|
||||
RoutingMode,
|
||||
RoutingOutcome,
|
||||
RoutingRequest,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("cowork_local.application.model_routing")
|
||||
|
||||
# Asks the user to approve a Manual-mode switch. Receives the underlying
|
||||
# decision object (for rendering) plus the timeout in seconds; returns True to
|
||||
# approve. Supplied by the caller so this module never imports a UI toolkit.
|
||||
ConfirmationCallback = Callable[[Any, float], bool]
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class RoutingDecisionPort(Protocol):
|
||||
"""The routing engine, as this service needs it.
|
||||
|
||||
Narrowed to a single method on purpose: the concrete engine
|
||||
(``core/routing/service.py::RoutingService``) exposes assessment,
|
||||
persistence and scheduling too, none of which a turn-time decision needs.
|
||||
"""
|
||||
|
||||
def evaluate(self, request: RoutingRequest, mode: RoutingMode) -> RouteEvaluation:
|
||||
"""Rank candidates for ``request`` and report whether to switch."""
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ModeResolver(Protocol):
|
||||
"""Resolves the effective routing mode for a surface.
|
||||
|
||||
In the app this reads the active workspace's per-surface override with the
|
||||
global default behind it (``AppContext.project_routing_mode``); in tests it
|
||||
is a two-line stub.
|
||||
"""
|
||||
|
||||
def mode_for(self, surface: str) -> RoutingMode:
|
||||
"""Effective mode for ``surface``."""
|
||||
|
||||
|
||||
class RoutingApplicationService:
|
||||
"""Turn-time routing decisions for every chat surface."""
|
||||
|
||||
# Matches DEFAULT_CONFIG["routing"]["confirm_timeout_sec"]; used only when
|
||||
# no timeout provider is wired, so a bare service is still usable in tests.
|
||||
DEFAULT_CONFIRM_TIMEOUT_SEC = 60.0
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
decision_port: RoutingDecisionPort,
|
||||
mode_resolver: Optional[ModeResolver] = None,
|
||||
*,
|
||||
confirm_timeout_sec: Optional[Callable[[], float]] = None,
|
||||
) -> None:
|
||||
self._decision_port = decision_port
|
||||
self._mode_resolver = mode_resolver
|
||||
# A callable rather than a number: the timeout lives in mutable config
|
||||
# the user can change in Settings between two turns.
|
||||
self._confirm_timeout_sec = confirm_timeout_sec
|
||||
|
||||
# -- public API ------------------------------------------------------ #
|
||||
def resolve(
|
||||
self,
|
||||
request: RoutingRequest,
|
||||
confirm: Optional[ConfirmationCallback] = None,
|
||||
) -> RoutingOutcome:
|
||||
"""Decide this turn's provider/model.
|
||||
|
||||
Returns a :class:`RoutingOutcome`; ``provider``/``model`` are ``None``
|
||||
whenever the surface should keep its own selection. Never raises — an
|
||||
unexpected failure is logged and reported as "keep current", because a
|
||||
broken assessment store must not block chatting.
|
||||
"""
|
||||
mode = request.mode or self._resolve_mode(request.surface)
|
||||
try:
|
||||
return self._resolve_unguarded(request, mode, confirm)
|
||||
except Exception: # noqa: BLE001 — routing must never break a turn
|
||||
logger.exception("routing.resolve failed — keeping the current model")
|
||||
return RoutingOutcome.keep_current(mode, reason="routing error — keeping current model")
|
||||
|
||||
def confirm_timeout(self) -> float:
|
||||
"""Seconds to wait for a Manual-mode confirmation.
|
||||
|
||||
Falls back to the built-in default when the provider is missing or
|
||||
returns something unusable, so a corrupted config value cannot produce a
|
||||
zero-second dialog that instantly declines every switch.
|
||||
"""
|
||||
if self._confirm_timeout_sec is None:
|
||||
return self.DEFAULT_CONFIRM_TIMEOUT_SEC
|
||||
try:
|
||||
value = float(self._confirm_timeout_sec())
|
||||
except (TypeError, ValueError):
|
||||
return self.DEFAULT_CONFIRM_TIMEOUT_SEC
|
||||
return value if value > 0 else self.DEFAULT_CONFIRM_TIMEOUT_SEC
|
||||
|
||||
# -- internals ------------------------------------------------------- #
|
||||
def _resolve_mode(self, surface: str) -> RoutingMode:
|
||||
"""The surface's configured mode, defaulting to OFF when unresolvable —
|
||||
routing stays opt-in, so "we don't know" must mean "don't switch"."""
|
||||
if self._mode_resolver is None:
|
||||
return RoutingMode.OFF
|
||||
try:
|
||||
return RoutingMode.parse(self._mode_resolver.mode_for(surface))
|
||||
except Exception: # noqa: BLE001 — a config read must not break a turn
|
||||
logger.exception("routing: could not resolve mode for surface %r", surface)
|
||||
return RoutingMode.OFF
|
||||
|
||||
def _resolve_unguarded(
|
||||
self,
|
||||
request: RoutingRequest,
|
||||
mode: RoutingMode,
|
||||
confirm: Optional[ConfirmationCallback],
|
||||
) -> RoutingOutcome:
|
||||
"""The decision flow proper; :meth:`resolve` owns the safety net."""
|
||||
# 1. Routing disabled, or nothing to classify -> keep the selection.
|
||||
if mode is RoutingMode.OFF:
|
||||
return RoutingOutcome.keep_current(mode, reason="routing off")
|
||||
if not request.has_prompt:
|
||||
return RoutingOutcome.keep_current(mode, reason="empty prompt — nothing to route")
|
||||
|
||||
# 2. Ask the engine. FALLBACK is evaluated with AUTO's ranking because
|
||||
# it needs the same candidate list; only the accept/reject rule below
|
||||
# differs, so the engine stays unaware of the extra mode.
|
||||
engine_mode = RoutingMode.AUTO if mode is RoutingMode.FALLBACK else mode
|
||||
evaluation = self._decision_port.evaluate(request, engine_mode)
|
||||
|
||||
# 3. Apply the mode's own accept rule to the engine's verdict.
|
||||
if mode is RoutingMode.FALLBACK:
|
||||
accepted, reason = self._fallback_verdict(evaluation)
|
||||
else:
|
||||
accepted, reason = evaluation.should_switch, evaluation.reason
|
||||
|
||||
if not accepted or not evaluation.has_target:
|
||||
return RoutingOutcome.keep_current(
|
||||
mode,
|
||||
reason=reason or evaluation.reason,
|
||||
task_type=evaluation.task_type,
|
||||
decision=evaluation.decision,
|
||||
)
|
||||
|
||||
# 4. Manual mode asks first; a decline or a timeout keeps the current
|
||||
# model (and is reported as such, so the surface can tell the two
|
||||
# cases apart from "nothing better was found").
|
||||
if mode is RoutingMode.MANUAL and not self._approved(evaluation, confirm):
|
||||
return RoutingOutcome.keep_current(
|
||||
mode,
|
||||
reason="switch declined by user or confirmation timed out",
|
||||
task_type=evaluation.task_type,
|
||||
declined=True,
|
||||
decision=evaluation.decision,
|
||||
)
|
||||
|
||||
# 5. Publish the override for THIS turn only. The provider falls back to
|
||||
# the request's current provider when the engine named a model but no
|
||||
# provider (same-provider switch).
|
||||
return RoutingOutcome(
|
||||
mode=mode,
|
||||
switched=True,
|
||||
provider=evaluation.target_provider or request.current_provider,
|
||||
model=evaluation.target_model or "",
|
||||
task_type=evaluation.task_type,
|
||||
score_gain=evaluation.score_gain,
|
||||
reason=reason or evaluation.reason,
|
||||
decision=evaluation.decision,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _fallback_verdict(evaluation: RouteEvaluation) -> tuple:
|
||||
"""FALLBACK's accept rule: switch ONLY to rescue an unusable selection.
|
||||
|
||||
The user's pinned model wins as long as it can serve the turn, even when
|
||||
a higher-scoring candidate exists — that is the whole point of the mode.
|
||||
A switch happens only when the current model is not a usable candidate
|
||||
(never assessed, marked unavailable, or its last probe failed) and the
|
||||
engine has something to move to.
|
||||
"""
|
||||
if evaluation.current_is_usable:
|
||||
return False, "fallback mode — current model is healthy, keeping it"
|
||||
if not evaluation.has_target:
|
||||
return False, "fallback mode — current model unusable and no replacement available"
|
||||
return True, "fallback mode — current model unavailable, switching to the best alternative"
|
||||
|
||||
def _approved(
|
||||
self,
|
||||
evaluation: RouteEvaluation,
|
||||
confirm: Optional[ConfirmationCallback],
|
||||
) -> bool:
|
||||
"""Run the Manual-mode confirmation callback.
|
||||
|
||||
No callback means no way to ask, and silently switching in Manual mode
|
||||
would violate the mode's contract — so a missing callback is treated as
|
||||
"not approved". A callback that raises is treated the same way, since a
|
||||
broken dialog must not auto-approve a model change.
|
||||
"""
|
||||
if confirm is None:
|
||||
logger.warning("routing: manual mode without a confirmation callback — keeping current model")
|
||||
return False
|
||||
try:
|
||||
return bool(confirm(evaluation.decision, self.confirm_timeout()))
|
||||
except Exception: # noqa: BLE001
|
||||
logger.exception("routing: confirmation callback failed — keeping current model")
|
||||
return False
|
||||
|
||||
|
||||
__all__ = [
|
||||
"ConfirmationCallback",
|
||||
"ModeResolver",
|
||||
"RoutingApplicationService",
|
||||
"RoutingDecisionPort",
|
||||
]
|
||||
@@ -0,0 +1,158 @@
|
||||
"""Pure-Python DTOs exchanged with :mod:`routing_application_service`.
|
||||
|
||||
These types are the vocabulary the chat surfaces (Cowork chat, Co4E, AI-Edit)
|
||||
now speak instead of each re-deriving routing state from raw config lookups and
|
||||
``core/routing`` internals.
|
||||
|
||||
Layer rules (``docs/architecture/ADR-001-layered-architecture.md``): application
|
||||
code is 100% pure Python. Nothing here imports PySide6, and nothing here imports
|
||||
``core.routing`` either — the concrete routing engine is reached only through
|
||||
the adapter in :mod:`core_routing_adapter`, which keeps this module trivially
|
||||
testable with plain fakes.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any, Optional, Tuple
|
||||
|
||||
|
||||
class RoutingMode(str, Enum):
|
||||
"""The four routing behaviours a surface can be in (R03-T03).
|
||||
|
||||
``OFF``/``AUTO``/``MANUAL`` map 1:1 onto the existing per-surface toggle and
|
||||
onto ``core/routing/models.py::SwitchMode``. ``FALLBACK`` is new and
|
||||
deliberately NOT an optimisation mode: it keeps whatever model the user
|
||||
chose and only re-routes when that model cannot serve the turn, which is the
|
||||
behaviour a resilience-minded workspace wants (never surprise me, but never
|
||||
leave me stuck either).
|
||||
"""
|
||||
|
||||
OFF = "off"
|
||||
AUTO = "auto"
|
||||
MANUAL = "manual"
|
||||
FALLBACK = "fallback"
|
||||
|
||||
@classmethod
|
||||
def parse(cls, raw: Any, default: "RoutingMode" = None) -> "RoutingMode":
|
||||
"""Best-effort coercion from config/UI strings.
|
||||
|
||||
Routing must never break a turn, so an unrecognised value degrades to
|
||||
``default`` (``OFF`` unless told otherwise) instead of raising — the same
|
||||
defensive posture ``config.routing_mode_for`` already takes.
|
||||
"""
|
||||
fallback = default if default is not None else cls.OFF
|
||||
if isinstance(raw, cls):
|
||||
return raw
|
||||
try:
|
||||
return cls(str(raw or "").strip().lower())
|
||||
except ValueError:
|
||||
return fallback
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RoutingRequest:
|
||||
"""Everything needed to decide how ONE turn should be routed.
|
||||
|
||||
Frozen: the request is captured from live UI state (the selected model, the
|
||||
typed prompt) and then handed to code that may run on a worker thread. An
|
||||
immutable snapshot means the user changing the model picker mid-turn cannot
|
||||
retroactively alter the decision that was already made — the same rationale
|
||||
behind R04's ``ConversationExecutionRequest``.
|
||||
"""
|
||||
|
||||
surface: str # "cowork" | "co4e" | "ai_edit" | ...
|
||||
prompt: str # the user's text; drives task classification
|
||||
current_provider: str # provider the surface would use as-is
|
||||
current_model: str = "" # model the surface would use ("" = provider default)
|
||||
mode: Optional[RoutingMode] = None # explicit override; None -> resolve per surface
|
||||
# Pre-classified task type ("coding", "qa", ...). AI-Edit always knows its
|
||||
# turns are coding work, so it pins this and skips prompt classification.
|
||||
task_type: Optional[str] = None
|
||||
required_capabilities: Tuple[str, ...] = () # e.g. ("vision",)
|
||||
|
||||
@property
|
||||
def has_prompt(self) -> bool:
|
||||
"""Whether there is anything to classify. An empty prompt cannot be
|
||||
routed meaningfully, so every surface short-circuits on it."""
|
||||
return bool((self.prompt or "").strip())
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RouteEvaluation:
|
||||
"""A routing engine's verdict, normalised away from ``core/routing`` types.
|
||||
|
||||
The adapter flattens ``RouteResult``/``SwitchDecision`` into these plain
|
||||
fields so the application service never touches Pydantic models or enums
|
||||
owned by another layer. ``decision`` still carries the original object
|
||||
because the Manual-mode confirm dialog renders its ``reason``.
|
||||
"""
|
||||
|
||||
task_type: str
|
||||
should_switch: bool
|
||||
target_provider: Optional[str] = None
|
||||
target_model: Optional[str] = None
|
||||
score_gain: float = 0.0
|
||||
reason: str = ""
|
||||
# False when the currently selected model is not a usable candidate for this
|
||||
# task (unranked, unavailable, or failed its last probe) — the single signal
|
||||
# FALLBACK mode acts on.
|
||||
current_is_usable: bool = True
|
||||
decision: Any = None # original SwitchDecision, for the UI dialog
|
||||
|
||||
@property
|
||||
def has_target(self) -> bool:
|
||||
"""A switch is only actionable when the engine named a model to move to."""
|
||||
return bool(self.target_model or self.target_provider)
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class RoutingOutcome:
|
||||
"""What the calling surface should actually do for this turn.
|
||||
|
||||
A surface needs exactly three things from routing — "which provider/model do
|
||||
I build?", "do I tell the user?" and "was I told to stand down?" — so those
|
||||
are the fields here, and nothing else. ``provider``/``model`` are ``None``
|
||||
when the surface should keep its own selection untouched.
|
||||
"""
|
||||
|
||||
mode: RoutingMode
|
||||
switched: bool = False
|
||||
provider: Optional[str] = None
|
||||
model: Optional[str] = None
|
||||
task_type: str = ""
|
||||
score_gain: float = 0.0
|
||||
reason: str = ""
|
||||
# True when Manual mode proposed a switch and the user declined or the
|
||||
# confirmation timed out. Distinct from "no switch proposed" so a surface
|
||||
# can tell "routing had nothing to offer" from "the user said no".
|
||||
declined: bool = False
|
||||
decision: Any = field(default=None, repr=False)
|
||||
|
||||
@property
|
||||
def should_notify(self) -> bool:
|
||||
"""Whether the surface should post the "switched model" status bubble.
|
||||
Only an executed switch is worth interrupting the transcript for."""
|
||||
return self.switched
|
||||
|
||||
@classmethod
|
||||
def keep_current(
|
||||
cls,
|
||||
mode: RoutingMode,
|
||||
*,
|
||||
reason: str = "",
|
||||
task_type: str = "",
|
||||
declined: bool = False,
|
||||
decision: Any = None,
|
||||
) -> "RoutingOutcome":
|
||||
"""The no-change outcome — the single constructor for every path that
|
||||
leaves the surface's own model selection in place (routing off, empty
|
||||
prompt, no better candidate, user declined, internal error)."""
|
||||
return cls(
|
||||
mode=mode, switched=False, provider=None, model=None,
|
||||
task_type=task_type, reason=reason, declined=declined, decision=decision,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["RoutingMode", "RoutingRequest", "RouteEvaluation", "RoutingOutcome"]
|
||||
@@ -0,0 +1 @@
|
||||
"""Application monitoring package: Monitoring query service for audit and metrics."""
|
||||
@@ -0,0 +1,47 @@
|
||||
"""Application-layer view of an audit event — decoupled from the
|
||||
infrastructure ``CanonicalAuditEvent`` so ``application/`` doesn't need to
|
||||
share a concrete class with ``infrastructure/`` (only the shape). Field names
|
||||
match the canonical audit schema (see
|
||||
``infrastructure/telemetry/audit_logger.py``) 1:1.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AuditEventDTO:
|
||||
ts: str
|
||||
kind: str
|
||||
name: str
|
||||
ok: bool
|
||||
detail: str
|
||||
agent_role: str = ""
|
||||
account: str = ""
|
||||
role: str = ""
|
||||
machine: str = ""
|
||||
|
||||
@classmethod
|
||||
def from_raw(cls, raw: Dict[str, Any]) -> "AuditEventDTO":
|
||||
"""Tolerant of missing keys — accepts both a
|
||||
``CanonicalAuditEvent.to_dict()`` result and any historical raw
|
||||
``.jsonl`` row."""
|
||||
return cls(
|
||||
ts=str(raw.get("ts", "")),
|
||||
kind=str(raw.get("kind", "")),
|
||||
name=str(raw.get("name", "")),
|
||||
ok=bool(raw.get("ok", False)),
|
||||
detail=str(raw.get("detail", "")),
|
||||
agent_role=str(raw.get("agent_role", "")),
|
||||
account=str(raw.get("account", "")),
|
||||
role=str(raw.get("role", "")),
|
||||
machine=str(raw.get("machine", "")),
|
||||
)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"ts": self.ts, "kind": self.kind, "agent_role": self.agent_role,
|
||||
"name": self.name, "ok": self.ok, "detail": self.detail,
|
||||
"account": self.account, "role": self.role, "machine": self.machine,
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
"""Read-only query service over audit events — filter + sort + pagination.
|
||||
|
||||
Pure Python: no PySide6 import, no UI code. Depends only on an injected
|
||||
``AuditEventRepository`` (see ``repository/audit_event_repository.py``), so it
|
||||
is fully unit-testable with ``InMemoryAuditEventRepository`` and independent
|
||||
of file I/O or Qt.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional
|
||||
|
||||
from .dto.audit_event_dto import AuditEventDTO
|
||||
from .repository.audit_event_repository import AuditEventRepository
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Page:
|
||||
items: List[AuditEventDTO]
|
||||
total: int
|
||||
page: int
|
||||
page_size: int
|
||||
|
||||
@property
|
||||
def has_more(self) -> bool:
|
||||
return self.page * self.page_size < self.total
|
||||
|
||||
|
||||
class MonitoringQueryService:
|
||||
"""Read-only. Callers ask for a filtered/sorted/paginated slice of the
|
||||
audit log; this service never writes anything."""
|
||||
|
||||
def __init__(self, repository: AuditEventRepository) -> None:
|
||||
self._repository = repository
|
||||
|
||||
def query(self, kind: Optional[str] = None, ok: Optional[bool] = None,
|
||||
text: Optional[str] = None, sort_by: str = "ts",
|
||||
descending: bool = True, page: int = 1, page_size: int = 50) -> Page:
|
||||
events = self._repository.load(kind=kind)
|
||||
|
||||
if ok is not None:
|
||||
events = [e for e in events if e.ok == ok]
|
||||
if text:
|
||||
needle = text.lower()
|
||||
events = [e for e in events
|
||||
if needle in e.name.lower() or needle in e.detail.lower()]
|
||||
|
||||
events = sorted(events, key=lambda e: getattr(e, sort_by, ""), reverse=descending)
|
||||
|
||||
total = len(events)
|
||||
page = max(1, page)
|
||||
start = (page - 1) * page_size
|
||||
items = events[start:start + page_size] if page_size > 0 else events
|
||||
return Page(items=items, total=total, page=page, page_size=page_size)
|
||||
@@ -0,0 +1,41 @@
|
||||
"""Audit-event repository — the boundary between ``MonitoringQueryService``
|
||||
and where events actually live. ``CanonicalAuditEventRepository`` is the real
|
||||
adapter (wraps an injected ``CanonicalAuditLogger``); ``InMemoryAuditEventRepository``
|
||||
is a constructor-injected test double, following this repo's existing
|
||||
``Fake*``/``Recording*`` convention (see ``tests/routing/*``,
|
||||
``tests/test_project_context_mcp_template.py``) rather than ``unittest.mock``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import List, Optional, Protocol
|
||||
|
||||
from ..dto.audit_event_dto import AuditEventDTO
|
||||
|
||||
|
||||
class AuditEventRepository(Protocol):
|
||||
def load(self, kind: Optional[str] = None) -> List[AuditEventDTO]:
|
||||
...
|
||||
|
||||
|
||||
class CanonicalAuditEventRepository:
|
||||
"""Adapter over ``infrastructure.telemetry.audit_logger.CanonicalAuditLogger``
|
||||
— the only place this application service reaches into infrastructure."""
|
||||
|
||||
def __init__(self, audit_logger) -> None:
|
||||
self._audit_logger = audit_logger
|
||||
|
||||
def load(self, kind: Optional[str] = None) -> List[AuditEventDTO]:
|
||||
events = self._audit_logger.load_events(kind=kind)
|
||||
return [AuditEventDTO.from_raw(e.to_dict()) for e in events]
|
||||
|
||||
|
||||
class InMemoryAuditEventRepository:
|
||||
"""Test double — holds a fixed list of events, no file I/O."""
|
||||
|
||||
def __init__(self, events: List[AuditEventDTO]) -> None:
|
||||
self._events = list(events)
|
||||
|
||||
def load(self, kind: Optional[str] = None) -> List[AuditEventDTO]:
|
||||
if kind is None:
|
||||
return list(self._events)
|
||||
return [e for e in self._events if e.kind == kind]
|
||||
@@ -0,0 +1 @@
|
||||
"""Application scheduling package: TaskApplicationService and AI task planning."""
|
||||
@@ -0,0 +1 @@
|
||||
"""Application settings package: Settings application service."""
|
||||
@@ -0,0 +1 @@
|
||||
"""Application workflows package: Co4E graph execution orchestration."""
|
||||
@@ -0,0 +1,379 @@
|
||||
"""``Co4EWorkflowService`` — nửa "hành vi" tách ra từ ``Co4ERunManager`` cũ.
|
||||
|
||||
Bối cảnh: ``core/co4e_run_manager.py::Co4ERunManager`` là một ``QObject`` gộp
|
||||
chung dữ liệu run (nay là ``domain/workflows/run_record.py::RunRecord``), logic
|
||||
chạy job trên ``AgentWorker``/``QThread``, và logic đọc/ghi lịch sử ra đĩa. File
|
||||
này là phần còn lại sau khi tách DTO: quản lý vòng đời nhiều run cùng lúc, các
|
||||
hook nhận sự kiện từ worker, và lưu/nạp lịch sử — nhưng THUẦN PYTHON, không kế
|
||||
thừa ``QObject`` và không tự dựng ``QThread`` (``application/`` cấm PySide6).
|
||||
|
||||
Hai điều thay ``Signal`` cũ:
|
||||
* ``changed = Signal()`` -> danh sách callback ``self._changed_callbacks`` +
|
||||
``on_changed(cb)`` để đăng ký; mọi chỗ code cũ gọi ``self.changed.emit()``
|
||||
nay gọi ``self._emit_changed()``, gọi callback theo ĐÚNG thứ tự đã đăng ký.
|
||||
* ``event = Signal(str, dict)`` -> ``self._event_callbacks`` + ``on_event(cb)``,
|
||||
tương tự, thay ``self.event.emit(rid, ev)`` bằng ``self._emit_event(rid, ev)``.
|
||||
* ``self.changed.connect(self._save_history)`` (lớp cũ tự nối signal của
|
||||
chính nó vào slot riêng, trong ``__init__``) -> ở đây gọi thẳng
|
||||
``self._save_history()`` làm bước ĐẦU TIÊN bên trong ``_emit_changed()``,
|
||||
trước khi chạy các callback đã đăng ký từ bên ngoài. Chọn cách "gọi thẳng"
|
||||
(thay vì "đăng ký như callback đầu tiên") vì nó khớp với thứ tự nối cũ
|
||||
(``_save_history`` luôn được nối sớm nhất trong ``__init__`` nên luôn chạy
|
||||
trước mọi slot ngoài nối sau) mà không cần một danh sách callback nội bộ
|
||||
riêng chỉ để chứa đúng một phần tử cố định.
|
||||
|
||||
``start()`` KHÔNG tự tạo ``AgentWorker``/``QThread`` — nó nhận một ``runner``
|
||||
(``WorkflowRunner`` Protocol, mặc định ``None``) tiêm qua constructor. Adapter
|
||||
Qt thật (bọc ``AgentWorker`` — xem ``core/worker.py``) là việc của widget ở
|
||||
``presentation/``, không viết ở đây; test dùng fake chạy đồng bộ
|
||||
(``tests/fakes/fake_co4e_workflow_service.py`` hoặc fake cục bộ trong
|
||||
``tests/test_co4e_workflow_service.py``).
|
||||
|
||||
KHÔNG xoá/sửa ``core/co4e_run_manager.py`` — lớp cũ tiếp tục chạy song song
|
||||
cho tới khi widget Co4E Studio thật (``ui/co4e_tab.py``) chuyển hẳn sang dùng
|
||||
service này.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from ...infrastructure.persistence.json.atomic_json_file import AtomicJsonFile
|
||||
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Callable, Dict, List, Optional, Protocol, Set
|
||||
|
||||
from ...core.co4e import CO4E_DIR, STEP_DONE, STEP_ERROR, STEP_PLANNED, Workflow, slugify, workflow_to_dict
|
||||
from ...domain.workflows.run_record import RunRecord
|
||||
|
||||
_TERMINAL_NODE = {STEP_DONE, STEP_ERROR, STEP_PLANNED}
|
||||
_HISTORY_CAP = 500 # giữ N run gần nhất trên đĩa
|
||||
|
||||
|
||||
def _now_str() -> str:
|
||||
return datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
|
||||
def _current_user() -> str:
|
||||
"""Best-effort creator name for a run (signed-in MS365 identity -> OS user)."""
|
||||
return os.environ.get("USERNAME") or os.environ.get("USER") or "you"
|
||||
|
||||
|
||||
# ---- ports (Protocol) — thay QThread thật bằng thứ tiêm được ---------------
|
||||
class RunnerJob(Protocol):
|
||||
"""Bề mặt tối thiểu mà job workflow cần từ 'worker' của nó.
|
||||
|
||||
Tương ứng ``AgentWorker.emit_event``/``AgentWorker.is_cancelled`` cũ
|
||||
(``core/worker.py``) — giữ nguyên chữ ký đó để hàm job bên trong
|
||||
``co4e_runner.run_workflow`` không phải đổi khi runner đứng sau là
|
||||
``AgentWorker``/``QThread`` thật (adapter ở presentation/) hay là fake
|
||||
đồng bộ trong test.
|
||||
"""
|
||||
|
||||
def emit_event(self, ev: dict) -> None: ...
|
||||
def is_cancelled(self) -> bool: ...
|
||||
|
||||
|
||||
class RunWorkerHandle(Protocol):
|
||||
"""Điều khiển một job đang chạy nền — tương ứng phần
|
||||
``AgentWorker.request_stop()`` cũ mà ``Co4ERunManager.stop()`` gọi."""
|
||||
|
||||
def request_stop(self) -> None: ...
|
||||
|
||||
|
||||
class WorkflowRunner(Protocol):
|
||||
"""Cổng chạy một job nền, tiêm qua constructor ``Co4EWorkflowService``.
|
||||
|
||||
Thay cho việc service tự ``AgentWorker(job); worker.start()`` (cần
|
||||
``QThread`` -> cấm ở ``application/``). Bên gọi ``start()`` truyền vào
|
||||
``job`` với đúng chữ ký cũ (``job(worker) -> Optional[dict]``); runner chịu
|
||||
trách nhiệm chạy nó (nền thật hay đồng bộ) và gọi lại ba callback tương ứng
|
||||
ba signal cũ của ``AgentWorker`` (``event``/``finished_ok``/``failed``).
|
||||
"""
|
||||
|
||||
def start(self, run_id: str, job: Callable[[RunnerJob], Optional[dict]],
|
||||
on_event: Callable[[dict], None],
|
||||
on_finished: Callable[[Optional[dict]], None],
|
||||
on_failed: Callable[[str], None]) -> RunWorkerHandle: ...
|
||||
|
||||
|
||||
class Co4EWorkflowService:
|
||||
"""Tầng application: vòng đời nhiều run Co4E cùng lúc, thuần Python.
|
||||
|
||||
Vai trò: đây là nơi ``build_co4e_tab(ctx, workflow_service)``
|
||||
(``presentation/co4e/co4e_tab.py``) sẽ lấy ``workflow_service`` thật một
|
||||
khi widget Co4E Studio được lắp lại để dùng nó — hiện widget thật
|
||||
(``ui/co4e_tab.py``) vẫn dùng ``Co4ERunManager`` cũ song song.
|
||||
"""
|
||||
|
||||
def __init__(self, ctx, *, history_path: Optional[Path] = None,
|
||||
runner: Optional[WorkflowRunner] = None):
|
||||
self.ctx = ctx
|
||||
self._runs: Dict[str, RunRecord] = {}
|
||||
self._worker_handles: Dict[str, RunWorkerHandle] = {}
|
||||
self._seq = 0
|
||||
self._output_root: Optional[Path] = None # thư mục output co4e của workspace đang chọn
|
||||
self._project_id: str = "" # workspace đang chọn — Flow Status lọc theo no
|
||||
self._runner = runner
|
||||
# DTO domain khong duoc cham dia (xem domain/workflows/run_record.py),
|
||||
# nen viec doc/ghi file lich su nam o day, tang application.
|
||||
self._history_path_value = (
|
||||
Path(history_path) if history_path is not None else (CO4E_DIR / "run_history.json")
|
||||
)
|
||||
self._changed_callbacks: List[Callable[[], None]] = []
|
||||
self._event_callbacks: List[Callable[[str, dict], None]] = []
|
||||
self._load_history() # khoi phuc lich su cu de Flow Status
|
||||
# giu du lich su qua cac lan restart
|
||||
|
||||
# ---- callback thay Signal ---------------------------------------------
|
||||
def on_changed(self, cb: Callable[[], None]) -> None:
|
||||
self._changed_callbacks.append(cb)
|
||||
|
||||
def on_event(self, cb: Callable[[str, dict], None]) -> None:
|
||||
self._event_callbacks.append(cb)
|
||||
|
||||
def _emit_changed(self) -> None:
|
||||
self._save_history() # xem docstring dau file: giu dung thu tu ban Qt cu
|
||||
for cb in self._changed_callbacks:
|
||||
cb()
|
||||
|
||||
def _emit_event(self, run_id: str, ev) -> None:
|
||||
for cb in self._event_callbacks:
|
||||
cb(run_id, ev)
|
||||
|
||||
# ---- persistence --------------------------------------------------
|
||||
# Doc/ghi thu cong (json.loads/write_text + tmp.replace), KHONG dung
|
||||
# AtomicJsonFile — ban dau file nay dung AtomicJsonFile.read(), nhung
|
||||
# review phat hien no doi hanh vi that so voi Co4ERunManager cu: gap
|
||||
# JSON hong, AtomicJsonFile.read() ĐOI TEN file hong thanh
|
||||
# "<ten>.bad-<timestamp>" (quarantine) roi moi tra ve mac dinh, trong
|
||||
# khi ban cu chi bat loi va ĐE NGUYEN file hong tai cho, khong dong gi
|
||||
# vao no. Day la mot thay doi quan sat duoc tren dia ma khong test nao
|
||||
# khoa lai va khong co comment bao truoc — Lam (N3) da quyet 24/08:
|
||||
# GIU HANH VI CU nguyen van (khong quarantine), vi day la buoc tach
|
||||
# chi duoc phep doi hanh vi khi da noi ra ro rang va co lưới an toan,
|
||||
# khong phai luc nay.
|
||||
def _load_history(self) -> None:
|
||||
try:
|
||||
data = json.loads(self._history_path_value.read_text(encoding="utf-8"))
|
||||
except (OSError, ValueError):
|
||||
return
|
||||
max_seq = 0
|
||||
for rec in data.get("runs", []):
|
||||
try:
|
||||
record = RunRecord.from_dict(rec)
|
||||
except Exception:
|
||||
continue
|
||||
if not record.id:
|
||||
continue
|
||||
self._runs[record.id] = record
|
||||
if record.id.startswith("run") and record.id[3:].isdigit():
|
||||
max_seq = max(max_seq, int(record.id[3:]))
|
||||
self._seq = max_seq # tranh sinh id trung voi lich su
|
||||
|
||||
def _save_history(self) -> None:
|
||||
runs = list(self._runs.values())[-_HISTORY_CAP:]
|
||||
payload = {"runs": [r.to_dict() for r in runs]}
|
||||
try:
|
||||
self._history_path_value.parent.mkdir(parents=True, exist_ok=True)
|
||||
# AtomicJsonFile thay cho tmp+replace tự viết: bản cũ thiếu fsync
|
||||
# (dữ liệu có thể còn trong bộ đệm khi mất điện) và dùng thẳng
|
||||
# Path.replace, vốn thỉnh thoảng bị Defender từ chối trên Windows.
|
||||
AtomicJsonFile(self._history_path_value).write(payload)
|
||||
except OSError:
|
||||
# Giu dung hanh vi cu (core/co4e_run_manager.py::_save_history):
|
||||
# mot lan luu that bai (day dia, mat quyen...) KHONG duoc phep
|
||||
# chan luong goi cua moi hook (_on_event/_on_finished/_on_failed)
|
||||
# dang di qua _emit_changed(). Bo try/except nay se lam mot loi
|
||||
# ghi dia lam vo ca luot xu ly su kien dang chay, chi vi lich su
|
||||
# khong luu duoc lan nay -- nguoi dung van thay Flow Status dung
|
||||
# trong phien hien tai, chi la ban ghi tren dia lui lai mot buoc.
|
||||
pass
|
||||
|
||||
# ---- lifecycle ----------------------------------------------------
|
||||
def _next_id(self) -> str:
|
||||
self._seq += 1
|
||||
return f"run{self._seq}"
|
||||
|
||||
def start(self, wf: Workflow, *, skill_map: Optional[Dict[str, str]] = None,
|
||||
plan_mode: bool = False, only_nodes: Optional[set] = None,
|
||||
seed_outputs: Optional[Dict[str, str]] = None,
|
||||
manual: bool = False, label: Optional[str] = None) -> str:
|
||||
"""Đăng ký một run mới và giao job cho ``self._runner`` (nếu có).
|
||||
|
||||
Không tự thực thi AI thật ở đây: khi ``self._runner`` là ``None``
|
||||
(mặc định), run được ghi nhận nhưng không job nào được giao đi — dùng
|
||||
cho test/khi chưa lắp adapter Qt thật.
|
||||
"""
|
||||
run_id = self._next_id()
|
||||
total = len(only_nodes) if only_nodes else len(wf.nodes)
|
||||
record = RunRecord(run_id, wf.id, label or wf.name, total, plan_mode, manual,
|
||||
created_by=_current_user(), created_at=_now_str(),
|
||||
project_id=self._project_id)
|
||||
# workflow_to_dict() tu dung dataclasses.asdict() de dung ca cay (node,
|
||||
# step, sub-agent) -> ban than no da la mot "deep copy" sang dict moi,
|
||||
# khong con giu tham chieu toi wf.nodes/wf.edges song. Vi vay KHONG can
|
||||
# deepcopy(wf) truoc nhu ban Qt cu (RunHandle.wf giu nguyen doi tuong
|
||||
# Workflow) -- xem doc string dau file domain/workflows/run_record.py
|
||||
# ve ly do snapshot o day la dict tho chu khong phai doi tuong.
|
||||
record.wf = workflow_to_dict(wf)
|
||||
nodes = list(wf.nodes)
|
||||
edges = list(wf.edges)
|
||||
out_dir = self._out_dir(wf)
|
||||
record.out_dir = str(out_dir)
|
||||
ctx = self.ctx
|
||||
sk = dict(skill_map or {})
|
||||
only: Optional[Set[str]] = set(only_nodes) if only_nodes else None
|
||||
seed = dict(seed_outputs or {})
|
||||
run_label = record.name
|
||||
self._runs[run_id] = record
|
||||
|
||||
if self._runner is not None:
|
||||
def job(worker: RunnerJob):
|
||||
from ...core import co4e_runner
|
||||
return co4e_runner.run_workflow(
|
||||
ctx, nodes, edges, out_dir, worker.emit_event, worker.is_cancelled,
|
||||
plan_mode=plan_mode, skill_map=sk, only_nodes=only, seed_outputs=seed,
|
||||
usage_label=run_label)
|
||||
|
||||
self._worker_handles[run_id] = self._runner.start(
|
||||
run_id, job,
|
||||
on_event=lambda ev, rid=run_id: self._on_event(rid, ev),
|
||||
on_finished=lambda _r=None, rid=run_id: self._on_finished(rid),
|
||||
on_failed=lambda e, rid=run_id: self._on_failed(rid, e),
|
||||
)
|
||||
self._emit_changed()
|
||||
return run_id
|
||||
|
||||
# ---- worker callbacks (goi tu runner, thay slot Qt cu) -----------------
|
||||
def _on_event(self, run_id: str, ev) -> None:
|
||||
record = self._runs.get(run_id)
|
||||
if record is not None and isinstance(ev, dict):
|
||||
t = ev.get("type")
|
||||
if t == "node_status":
|
||||
record.node_status[ev.get("node_id")] = ev.get("status")
|
||||
record.done = sum(1 for s in record.node_status.values() if s in _TERMINAL_NODE)
|
||||
self._emit_changed()
|
||||
elif t == "run_done":
|
||||
if record.status == "running":
|
||||
record.status = "done" if ev.get("ok", True) else "error"
|
||||
self._emit_changed()
|
||||
# quirk co y giu nguyen (xem test_on_event_unknown_run_id... trong ca
|
||||
# test cu lan test moi): re-emit VO DIEU KIEN, ke ca run_id la hoac ev
|
||||
# khong phai dict/None -- khac _on_finished/_on_failed la no-op hoan
|
||||
# toan khi run_id la.
|
||||
#
|
||||
# Khac biet CO CHU Y so voi ban Qt cu: Signal(str, dict) cua PySide6 ep
|
||||
# ev=None thanh {} khi giao cho slot (tac dung phu cua kieu Signal khai
|
||||
# bao cung). O day khong con Signal nen callback nhan DUNG gia tri ev
|
||||
# goc (None neu goi voi None) -- khong gia lap lai viec ep kieu do vi
|
||||
# no la tac dung phu cua Qt, khong phai quy tac nghiep vu can giu.
|
||||
self._emit_event(run_id, ev)
|
||||
|
||||
def _on_finished(self, run_id: str) -> None:
|
||||
record = self._runs.get(run_id)
|
||||
if record is not None and record.status == "running":
|
||||
# job returned without a run_done event (shouldn't happen) — settle it
|
||||
record.status = "done"
|
||||
self._emit_changed()
|
||||
|
||||
def _on_failed(self, run_id: str, err: str) -> None:
|
||||
record = self._runs.get(run_id)
|
||||
if record is not None:
|
||||
record.status = "error"
|
||||
record.error = str(err)
|
||||
self._emit_event(run_id, {"type": "run_error", "error": str(err)})
|
||||
self._emit_changed()
|
||||
|
||||
# ---- control --------------------------------------------------------
|
||||
def stop(self, run_id: str) -> None:
|
||||
record = self._runs.get(run_id)
|
||||
worker = self._worker_handles.get(run_id)
|
||||
if record is not None and worker is not None and record.running:
|
||||
worker.request_stop()
|
||||
record.status = "stopped"
|
||||
self._emit_changed()
|
||||
|
||||
def stop_all(self) -> None:
|
||||
# Only the CURRENT workspace's runs (Flow Status is per-project).
|
||||
for run_id in [r for r, rec in self._runs.items() if self._belongs(rec)]:
|
||||
self.stop(run_id)
|
||||
|
||||
def rename(self, run_id: str, new_name: str) -> None:
|
||||
"""Rename a run in the Flow Status history (and its kept workflow snapshot),
|
||||
then persist + refresh views. No-op on a blank name / unknown run."""
|
||||
record = self._runs.get(run_id)
|
||||
new_name = (new_name or "").strip()
|
||||
if record is None or not new_name or new_name == record.name:
|
||||
return
|
||||
record.name = new_name
|
||||
# DTO doi: RunHandle.wf cu la doi tuong Workflow (gan record.wf.name),
|
||||
# RunRecord.wf o day la dict tho (xem domain/workflows/run_record.py)
|
||||
# nen doi truc tiep khoa "name" cua dict thay vi thuoc tinh doi tuong.
|
||||
if record.wf is not None:
|
||||
record.wf["name"] = new_name
|
||||
self._emit_changed()
|
||||
|
||||
def remove(self, run_id: str) -> None:
|
||||
record = self._runs.get(run_id)
|
||||
if record is not None and record.running:
|
||||
self.stop(run_id)
|
||||
self._runs.pop(run_id, None)
|
||||
self._worker_handles.pop(run_id, None)
|
||||
self._emit_changed()
|
||||
|
||||
def clear_finished(self) -> None:
|
||||
# Only clear finished runs of the CURRENT workspace.
|
||||
for run_id in [r for r, rec in self._runs.items() if not rec.running and self._belongs(rec)]:
|
||||
self._runs.pop(run_id, None)
|
||||
self._worker_handles.pop(run_id, None)
|
||||
self._emit_changed()
|
||||
|
||||
# ---- queries ----------------------------------------------------------
|
||||
def _belongs(self, r: RunRecord) -> bool:
|
||||
"""Whether a run belongs to the currently-selected workspace."""
|
||||
return getattr(r, "project_id", "") == self._project_id
|
||||
|
||||
def runs(self) -> List[RunRecord]:
|
||||
"""Runs of the CURRENT workspace only — Flow Status is per-project."""
|
||||
return [r for r in self._runs.values() if self._belongs(r)]
|
||||
|
||||
def all_runs(self) -> List[RunRecord]:
|
||||
"""Every tracked run across all workspaces (background tracking)."""
|
||||
return list(self._runs.values())
|
||||
|
||||
def get(self, run_id: str) -> Optional[RunRecord]:
|
||||
return self._runs.get(run_id)
|
||||
|
||||
def active_count(self) -> int:
|
||||
return sum(1 for r in self._runs.values() if r.running and self._belongs(r))
|
||||
|
||||
def set_current_project(self, project_id: str) -> None:
|
||||
"""Filter Flow Status (and new runs) to this workspace. Runs started while
|
||||
this is set are tagged with it; the Runs view shows only matching runs."""
|
||||
pid = project_id or ""
|
||||
if pid != self._project_id:
|
||||
self._project_id = pid
|
||||
self._emit_changed() # re-render Flow Status for the new workspace
|
||||
|
||||
def set_output_root(self, root: Optional[Path]) -> None:
|
||||
"""Point flow outputs at the SELECTED workspace's co4e folder (set by the
|
||||
Co4E tab when a project is chosen). ``None`` → fall back to the global
|
||||
Cowork output dir."""
|
||||
self._output_root = Path(root) if root else None
|
||||
|
||||
def _out_dir(self, wf: Workflow) -> Path:
|
||||
# Flow deliverables are written into the SELECTED workspace (the active
|
||||
# project's folder) so they land where the user works with files (Folder
|
||||
# tab), not in the config/install folder. One subfolder per flow keeps
|
||||
# runs tidy. Falls back to the global Cowork output dir when no workspace
|
||||
# is selected.
|
||||
base = self._output_root
|
||||
if base is None:
|
||||
try:
|
||||
base = self.ctx.config.cowork_output_dir() / "co4e"
|
||||
except Exception: # noqa: BLE001 - fall back to the config dir if unavailable
|
||||
base = CO4E_DIR / "runs" / "co4e"
|
||||
d = Path(base) / slugify(wf.name or "flow")
|
||||
d.mkdir(parents=True, exist_ok=True)
|
||||
return d
|
||||
@@ -0,0 +1 @@
|
||||
"""Application workspaces package: File workspace and AI file editor services."""
|
||||
@@ -16,6 +16,8 @@ import json
|
||||
import os
|
||||
from dataclasses import dataclass, field
|
||||
from pathlib import Path
|
||||
|
||||
from .infrastructure.config.json_config_repository import JsonConfigRepository
|
||||
from typing import Any, Dict, List
|
||||
|
||||
CONFIG_DIR = Path.home() / ".cowork_local"
|
||||
@@ -343,274 +345,33 @@ def _migrate_connectors(data: Dict[str, Any]) -> None:
|
||||
data["mcp_servers"] = [] # migrated — the UI no longer manages this
|
||||
|
||||
|
||||
@dataclass
|
||||
class AppConfig:
|
||||
"""In-memory view of the configuration with load/save helpers."""
|
||||
class AppConfig(JsonConfigRepository):
|
||||
"""Vỏ tương thích — R02 đã thay lớp này bằng :class:`JsonConfigRepository`.
|
||||
|
||||
data: Dict[str, Any] = field(default_factory=lambda: copy.deepcopy(DEFAULT_CONFIG))
|
||||
path: Path = CONFIG_PATH
|
||||
Ngày 25/08 app chuyển hẳn sang repository (ghi nguyên tử, khoá nằm trong
|
||||
kho bí mật của hệ điều hành). Nhưng cái tên ``AppConfig`` còn nằm ở 41 file
|
||||
— 23 checker trong ``tools/`` và 18 file test, trong đó có test của cả ba
|
||||
người. Sửa hết 41 chỗ trong một commit là đổi thứ không cần đổi và làm
|
||||
review không đọc nổi.
|
||||
|
||||
Nên giữ tên, đổi ruột: mọi lối vào đều dẫn tới repository.
|
||||
|
||||
Bỏ hẳn được khi ``tools/`` và ``tests/`` chuyển sang gọi
|
||||
``presentation.shell.bootstrap.build_context()``.
|
||||
"""
|
||||
|
||||
def __init__(self, data=None, path: Path = CONFIG_PATH, **kw):
|
||||
if data is None:
|
||||
super().__init__(Path(path), **kw)
|
||||
return
|
||||
# Dạng AppConfig(data=..., path=...) mà 13 file test đang dùng: dựng
|
||||
# thẳng từ dict, không đụng đĩa.
|
||||
built = JsonConfigRepository.from_data(data, Path(path))
|
||||
self.__dict__.update(built.__dict__)
|
||||
|
||||
# ---- persistence -------------------------------------------------
|
||||
@classmethod
|
||||
def load(cls, path: Path = CONFIG_PATH) -> "AppConfig":
|
||||
merged = copy.deepcopy(DEFAULT_CONFIG)
|
||||
if path.exists():
|
||||
try:
|
||||
stored = json.loads(path.read_text(encoding="utf-8"))
|
||||
merged = _deep_merge(merged, stored)
|
||||
except (json.JSONDecodeError, OSError):
|
||||
# Corrupt config should never block startup.
|
||||
merged = copy.deepcopy(DEFAULT_CONFIG)
|
||||
merged = _apply_env_overrides(merged)
|
||||
# "unlocked" is a runtime-only Settings-panel state (see the "ms365"
|
||||
# comment in DEFAULT_CONFIG) — never trust a stored/hand-edited value,
|
||||
# every launch starts locked.
|
||||
merged.setdefault("ms365", {})["unlocked"] = False
|
||||
_migrate_connectors(merged) # office→ms365 + legacy mcp_servers→other
|
||||
return cls(data=merged, path=path)
|
||||
|
||||
def save(self) -> None:
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
to_write = self.data
|
||||
if self.data.get("ms365", {}).get("unlocked"):
|
||||
# Defense in depth: even if some caller saves without having gone
|
||||
# through the Settings dialog's own auto-lock-after-save flow, the
|
||||
# unlock state must never reach disk.
|
||||
to_write = copy.deepcopy(self.data)
|
||||
to_write["ms365"]["unlocked"] = False
|
||||
self.path.write_text(
|
||||
json.dumps(to_write, indent=2, ensure_ascii=False), encoding="utf-8"
|
||||
)
|
||||
|
||||
# ---- convenience accessors --------------------------------------
|
||||
@property
|
||||
def active_provider(self) -> str:
|
||||
# Migrate configs that still point at a removed provider (e.g. an older
|
||||
# install saved "ollama") to a supported one, so the app never tries to
|
||||
# build an unknown provider.
|
||||
val = self.data.get("active_provider", "openai_compat")
|
||||
return val if val in PROVIDER_LABELS else "openai_compat"
|
||||
|
||||
@active_provider.setter
|
||||
def active_provider(self, value: str) -> None:
|
||||
self.data["active_provider"] = value
|
||||
|
||||
def provider_conf(self, name: str | None = None) -> Dict[str, Any]:
|
||||
name = name or self.active_provider
|
||||
return self.data["providers"].get(name, {})
|
||||
|
||||
@property
|
||||
def ca_bundle(self) -> str:
|
||||
"""Path to a custom CA/certificate PEM file, or '' for normal validation.
|
||||
|
||||
Used as ``requests``' ``verify=`` argument for every outbound HTTPS call
|
||||
— see the "tls_ca_bundle" comment above for when this is needed."""
|
||||
return (self.data.get("tls_ca_bundle") or "").strip()
|
||||
|
||||
@ca_bundle.setter
|
||||
def ca_bundle(self, value: str) -> None:
|
||||
self.data["tls_ca_bundle"] = (value or "").strip()
|
||||
|
||||
# ---- Microsoft 365 connections (Settings-panel lock, see DEFAULT_CONFIG) --
|
||||
@property
|
||||
def ms365(self) -> Dict[str, Any]:
|
||||
return self.data.setdefault("ms365", copy.deepcopy(DEFAULT_CONFIG["ms365"]))
|
||||
|
||||
# ---- Login / RBAC / shared cross-machine store (see DEFAULT_CONFIG) ------
|
||||
@property
|
||||
def auth(self) -> Dict[str, Any]:
|
||||
return self.data.setdefault("auth", copy.deepcopy(DEFAULT_CONFIG["auth"]))
|
||||
|
||||
@property
|
||||
def shared_dir(self) -> str:
|
||||
return (self.auth.get("shared_dir") or "").strip()
|
||||
|
||||
def ms365_try_unlock(self, code: str) -> bool:
|
||||
"""Unlock the MS365 Settings group for this session if ``code`` matches.
|
||||
|
||||
This is a client-side UI lock (prevents casually toggling a sensitive
|
||||
section), NOT Microsoft authentication — see the DEFAULT_CONFIG
|
||||
comment. Never persisted as unlocked; see ``save()``."""
|
||||
if (code or "") and code == self.ms365.get("unlock_code", ""):
|
||||
self.data["ms365"]["unlocked"] = True
|
||||
return True
|
||||
return False
|
||||
|
||||
def ms365_lock(self) -> None:
|
||||
self.data.setdefault("ms365", {})["unlocked"] = False
|
||||
|
||||
@property
|
||||
def theme(self) -> str:
|
||||
return self.data.get("theme", "dark")
|
||||
|
||||
@theme.setter
|
||||
def theme(self, value: str) -> None:
|
||||
self.data["theme"] = value
|
||||
|
||||
@property
|
||||
def language(self) -> str:
|
||||
from .i18n import DEFAULT_LANGUAGE, LANGUAGES
|
||||
val = self.data.get("language", DEFAULT_LANGUAGE)
|
||||
return val if val in LANGUAGES else DEFAULT_LANGUAGE
|
||||
|
||||
@language.setter
|
||||
def language(self, value: str) -> None:
|
||||
self.data["language"] = value
|
||||
|
||||
@property
|
||||
def code(self) -> Dict[str, Any]:
|
||||
return self.data["code"]
|
||||
|
||||
@property
|
||||
def tools_disabled(self) -> list:
|
||||
"""Built-in agent tool names the admin has turned off (Monitoring → Tools)."""
|
||||
return self.data.setdefault("tools", {}).setdefault("disabled", [])
|
||||
|
||||
def set_tool_enabled(self, name: str, enabled: bool) -> None:
|
||||
"""Enable/disable a built-in agent tool by name and persist it."""
|
||||
disabled = set(self.tools_disabled)
|
||||
if enabled:
|
||||
disabled.discard(name)
|
||||
else:
|
||||
disabled.add(name)
|
||||
self.data.setdefault("tools", {})["disabled"] = sorted(disabled)
|
||||
self.save()
|
||||
|
||||
@property
|
||||
def connect_external(self) -> bool:
|
||||
"""Master switch (Monitoring → Tools → Connector): when off, the agent
|
||||
connects to NO external connectors (CAD/CAE/MS365/Other MCP + REST).
|
||||
Defaults ON so existing setups keep working."""
|
||||
return bool(self.data.setdefault("tools", {}).get("connect_external", True))
|
||||
|
||||
def set_connect_external(self, enabled: bool) -> None:
|
||||
self.data.setdefault("tools", {})["connect_external"] = bool(enabled)
|
||||
self.save()
|
||||
|
||||
# ---- one-time seeding bookkeeping (built-in skill library / flows) -------
|
||||
@property
|
||||
def seeded_library_skills(self) -> List[str]:
|
||||
"""Slugs of bundled library skills already seeded into the user's Skill
|
||||
Manager — so a user-deleted one is never silently re-seeded."""
|
||||
return list(self.data.setdefault("seeded_library_skills", []))
|
||||
|
||||
@seeded_library_skills.setter
|
||||
def seeded_library_skills(self, slugs) -> None:
|
||||
self.data["seeded_library_skills"] = list(dict.fromkeys(slugs or []))
|
||||
|
||||
@property
|
||||
def seeded_builtin_flows(self) -> List[str]:
|
||||
"""Ids of built-in Co4E flows already seeded (same respect-user-deletion
|
||||
rule as seeded_library_skills)."""
|
||||
return list(self.data.setdefault("seeded_builtin_flows", []))
|
||||
|
||||
@seeded_builtin_flows.setter
|
||||
def seeded_builtin_flows(self, ids) -> None:
|
||||
self.data["seeded_builtin_flows"] = list(dict.fromkeys(ids or []))
|
||||
|
||||
@property
|
||||
def teams(self) -> Dict[str, Any]:
|
||||
return self.data["teams"]
|
||||
|
||||
@property
|
||||
def history(self) -> Dict[str, Any]:
|
||||
return self.data["history"]
|
||||
|
||||
@property
|
||||
def codebase_memory(self) -> Dict[str, Any]:
|
||||
return self.data["codebase_memory"]
|
||||
|
||||
@property
|
||||
def agent_security(self) -> Dict[str, Any]:
|
||||
return self.data["agent_security"]
|
||||
|
||||
@property
|
||||
def mcp_servers(self) -> List[Dict[str, Any]]:
|
||||
return self.data.setdefault("mcp_servers", [])
|
||||
|
||||
@property
|
||||
def ext_connectors(self) -> Dict[str, List[Dict[str, Any]]]:
|
||||
"""Unified Connectors (MCP), grouped by category CAD/CAE/MS365/Other —
|
||||
see core/ext_connectors.py for the per-entry shape and CATEGORIES."""
|
||||
d = self.data.setdefault("ext_connectors", {"cad": [], "cae": [], "ms365": [], "other": []})
|
||||
for cat in ("cad", "cae", "ms365", "other"):
|
||||
d.setdefault(cat, [])
|
||||
return d
|
||||
|
||||
@property
|
||||
def cowork(self) -> Dict[str, Any]:
|
||||
return self.data["cowork"]
|
||||
|
||||
@property
|
||||
def routing(self) -> Dict[str, Any]:
|
||||
"""Auto Model Assessment & Routing behaviour config (see DEFAULT_CONFIG).
|
||||
|
||||
Always returns a dict with every expected key present, backfilling any
|
||||
missing sub-keys from the defaults so older configs upgrade seamlessly."""
|
||||
d = self.data.setdefault("routing", copy.deepcopy(DEFAULT_CONFIG["routing"]))
|
||||
for k, v in DEFAULT_CONFIG["routing"].items():
|
||||
d.setdefault(k, copy.deepcopy(v))
|
||||
d.setdefault("surface_modes", {})
|
||||
for surface in ("cowork", "co4e", "ai_edit"):
|
||||
d["surface_modes"].setdefault(surface, "")
|
||||
return d
|
||||
|
||||
def routing_mode_for(self, surface: str) -> str:
|
||||
"""Effective Off/Auto/Manual mode for a chat surface.
|
||||
|
||||
A per-surface override ("auto"/"manual"/"off") wins; an empty override
|
||||
falls back to the global ``switch_mode``."""
|
||||
routing = self.routing
|
||||
override = (routing.get("surface_modes", {}) or {}).get(surface, "")
|
||||
mode = override or routing.get("switch_mode", "off")
|
||||
return mode if mode in ("off", "auto", "manual") else "off"
|
||||
|
||||
def set_routing_mode_for(self, surface: str, mode: str) -> None:
|
||||
"""Persist a chat surface's Off/Auto/Manual toggle selection."""
|
||||
mode = mode if mode in ("off", "auto", "manual") else "off"
|
||||
self.routing.setdefault("surface_modes", {})[surface] = mode
|
||||
self.save()
|
||||
|
||||
@property
|
||||
def structure(self) -> Dict[str, Any]:
|
||||
return self.data.setdefault("structure", {"max_nodes": 400, "max_edges": 400})
|
||||
|
||||
@property
|
||||
def monitoring_visibility(self) -> Dict[str, bool]:
|
||||
return self.data.setdefault(
|
||||
"monitoring_visibility", copy.deepcopy(DEFAULT_CONFIG["monitoring_visibility"]))
|
||||
|
||||
def cowork_output_dir(self) -> Path:
|
||||
"""Where Cowork saves generated files (OneDrive folder by default)."""
|
||||
custom = (self.cowork.get("output_dir") or "").strip()
|
||||
if custom:
|
||||
return Path(custom).expanduser()
|
||||
from . import paths # local import avoids any import cycle
|
||||
root = paths.primary_onedrive_root()
|
||||
if root is not None:
|
||||
return root / "CoworkLocal" / "output"
|
||||
return CONFIG_DIR / "output" / "cowork"
|
||||
|
||||
def history_dir(self) -> Path:
|
||||
"""Resolve where conversation history is stored.
|
||||
|
||||
When a project is open, its history is stored INSIDE the project's
|
||||
workspace folder (``_project_history_dir``, set by the Workspace screen)
|
||||
so that sharing/syncing that folder shares the history — another machine
|
||||
opening the same folder sees the conversations and can continue them.
|
||||
Otherwise: Local (default) or OneDrive."""
|
||||
rt = getattr(self, "_project_history_dir", None)
|
||||
if rt:
|
||||
return Path(rt)
|
||||
custom = (self.history.get("custom_dir") or "").strip()
|
||||
if custom:
|
||||
return Path(custom).expanduser()
|
||||
if self.history.get("location") == "onedrive":
|
||||
from . import paths # local import avoids any import cycle
|
||||
root = paths.primary_onedrive_root()
|
||||
if root is not None:
|
||||
return root / "CoworkLocal" / "history"
|
||||
return HISTORY_DIR
|
||||
|
||||
def model_label(self) -> str:
|
||||
return str(self.provider_conf().get("model", "?"))
|
||||
def load(cls, path: Path = CONFIG_PATH) -> "JsonConfigRepository":
|
||||
"""Điểm vào cũ. Giờ đi qua Composition Root nên checker và app dùng
|
||||
chung một đường dựng — kể cả phần ráp kho bí mật."""
|
||||
from .presentation.shell.bootstrap import build_config
|
||||
return build_config(Path(path))
|
||||
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
"""Root pytest conftest — loaded before ``tests/conftest.py``.
|
||||
|
||||
This checkout lives on disk as ``Refactor`` (not ``cowork_local``), while
|
||||
``tests/`` imports everything as ``from cowork_local... import ...`` and
|
||||
``tests/conftest.py`` makes that resolve by putting this repo's *parent*
|
||||
directory on ``sys.path`` (expecting the repo root itself to be named
|
||||
``cowork_local``). A sibling folder literally named ``cowork_local`` (an
|
||||
unrelated, older checkout) already exists next to this one, so without this
|
||||
file Python would silently import THAT folder instead of this repository
|
||||
whenever a test does ``import cowork_local``.
|
||||
|
||||
Registering the alias here — before ``tests/conftest.py`` touches
|
||||
``sys.path`` — caches this repository in ``sys.modules['cowork_local']``
|
||||
first, so the later ``sys.path`` mutation has nothing left to do (imports
|
||||
are cached by name; the first successful import of a given name wins for
|
||||
the rest of the process).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib.util
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
_ROOT = Path(__file__).resolve().parent
|
||||
|
||||
if "cowork_local" not in sys.modules:
|
||||
spec = importlib.util.spec_from_file_location(
|
||||
"cowork_local", _ROOT / "__init__.py", submodule_search_locations=[str(_ROOT)],
|
||||
)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
sys.modules["cowork_local"] = module
|
||||
spec.loader.exec_module(module)
|
||||
+2
-19
@@ -27,27 +27,12 @@ from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional
|
||||
|
||||
from ..providers.base import Provider
|
||||
from . import security_rules
|
||||
|
||||
|
||||
class SecurityBlocked(RuntimeError):
|
||||
"""A guardrail refused an action. ``verdict`` carries the full detail for
|
||||
the admin alert; ``str(exc)`` is the short, user-facing reason."""
|
||||
|
||||
def __init__(self, verdict: "SecurityVerdict"):
|
||||
super().__init__(verdict.reason or f"Blocked by agent security ({verdict.layer}).")
|
||||
self.verdict = verdict
|
||||
|
||||
|
||||
@dataclass
|
||||
class SecurityVerdict:
|
||||
allowed: bool
|
||||
reason: str = ""
|
||||
layer: str = "" # "prompt" | "attachment" | "command"
|
||||
from .agent_security_alert import notify_admin
|
||||
from .agent_security_types import SecurityBlocked, SecurityVerdict
|
||||
|
||||
|
||||
def combined_rules_text(config, max_chars: int = 8000, agent_kind: str = "cowork") -> str:
|
||||
@@ -236,7 +221,6 @@ def enforce_prompt(provider: Provider, messages: List[dict], config, emit,
|
||||
emit({"type": "notice", "level": "warning",
|
||||
"text": f"🛡 Yêu cầu bị chặn bởi Agent Security: {verdict.reason}"})
|
||||
from . import audit_log
|
||||
from .agent_security_alert import notify_admin
|
||||
|
||||
audit_log.record("security_block", "prompt", False, verdict.reason)
|
||||
notify_admin(config, verdict, detail=user_text[:1000])
|
||||
@@ -266,7 +250,6 @@ def enforce_command(provider: Provider, name: str, args: dict, config, emit,
|
||||
emit({"type": "notice", "level": "warning",
|
||||
"text": f"🛡 Lệnh bị chặn bởi Agent Security ({verdict.layer}): {verdict.reason}"})
|
||||
from . import audit_log
|
||||
from .agent_security_alert import notify_admin
|
||||
|
||||
audit_log.record("security_block", name, False, f"{verdict.layer}: {verdict.reason}")
|
||||
notify_admin(config, verdict, detail=command)
|
||||
|
||||
@@ -12,7 +12,7 @@ from __future__ import annotations
|
||||
from typing import Tuple
|
||||
|
||||
from . import ms365_graph
|
||||
from .agent_security import SecurityVerdict
|
||||
from .agent_security_types import SecurityVerdict
|
||||
from .ms365_auth import Ms365AuthError, get_access_token
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"""Shared value types for the Agent Security guardrails.
|
||||
|
||||
``SecurityVerdict``/``SecurityBlocked`` used to be defined in
|
||||
``agent_security.py``, which forced ``agent_security_alert.py`` (which only
|
||||
needs the *type*, to annotate/read ``notify_admin``'s ``verdict`` argument) to
|
||||
import from it — while ``agent_security.py`` itself needed to call
|
||||
``agent_security_alert.notify_admin()``, an architectural cycle only avoided
|
||||
at runtime by deferring that second import inside a function body.
|
||||
|
||||
Hoisting the shared type into this dependency-free leaf module lets both
|
||||
sides import it directly, so ``agent_security.py`` can import
|
||||
``agent_security_alert`` at module top level too — no cycle, no deferred
|
||||
imports needed for this pair.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass
|
||||
class SecurityVerdict:
|
||||
allowed: bool
|
||||
reason: str = ""
|
||||
layer: str = "" # "prompt" | "attachment" | "command"
|
||||
|
||||
|
||||
class SecurityBlocked(RuntimeError):
|
||||
"""A guardrail refused an action. ``verdict`` carries the full detail for
|
||||
the admin alert; ``str(exc)`` is the short, user-facing reason."""
|
||||
|
||||
def __init__(self, verdict: SecurityVerdict):
|
||||
super().__init__(verdict.reason or f"Blocked by agent security ({verdict.layer}).")
|
||||
self.verdict = verdict
|
||||
+15
-72
@@ -6,15 +6,23 @@ storage systems).
|
||||
One JSON line per event, one file per day under ``~/.cowork_local/audit/`` —
|
||||
same on-disk shape as ``usage_tracker.py`` (day-sharded ``.jsonl``, append-only,
|
||||
``record()`` never raises so audit logging can never break a chat turn).
|
||||
|
||||
This module is now a thin, backward-compatible wrapper around
|
||||
:class:`infrastructure.telemetry.audit_logger.CanonicalAuditLogger` — every
|
||||
existing call site (``agent_security.py``, ``chat_agent.py``, ``tools.py``,
|
||||
``ext_connectors.py``, ``mcp_client.py``, ``ms365_local.py``,
|
||||
``permissions.py``, ``ui/structure_graph_view.py``, ``app.py``) keeps calling
|
||||
``audit_log.set_identity``/``record``/``load_events`` exactly as before; only
|
||||
the implementation moved.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import date, datetime
|
||||
from datetime import date
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from ..config import CONFIG_DIR
|
||||
from ..infrastructure.telemetry.audit_logger import CanonicalAuditLogger
|
||||
|
||||
AUDIT_DIR = CONFIG_DIR / "audit"
|
||||
|
||||
@@ -23,66 +31,21 @@ AUDIT_DIR = CONFIG_DIR / "audit"
|
||||
# action), "mcp_call" (a call to an external MCP server's tool).
|
||||
Kind = str
|
||||
|
||||
# Process-global identity — who's logged in, their role, and this machine's
|
||||
# name — set once right after login (app.py::run()), mirroring
|
||||
# usage_tracker.py's identical pattern. NOT thread-local: fixed per process.
|
||||
_identity_account = ""
|
||||
_identity_role = ""
|
||||
_identity_machine = ""
|
||||
_identity_shared_dir = ""
|
||||
_logger = CanonicalAuditLogger(AUDIT_DIR)
|
||||
|
||||
|
||||
def set_identity(account: str, machine: str, role: str = "", shared_dir: str = "") -> None:
|
||||
"""Called once after login succeeds. ``shared_dir``, when reachable,
|
||||
makes every subsequent :func:`record` ALSO best-effort-append to the
|
||||
shared cross-machine telemetry store (see :mod:`telemetry_shared`)."""
|
||||
global _identity_account, _identity_role, _identity_machine, _identity_shared_dir
|
||||
_identity_account = account or ""
|
||||
_identity_role = role or ""
|
||||
_identity_machine = machine or ""
|
||||
_identity_shared_dir = shared_dir or ""
|
||||
_logger.set_identity(account, machine, role=role, shared_dir=shared_dir)
|
||||
|
||||
|
||||
def record(kind: Kind, name: str, ok: bool, detail: str = "",
|
||||
agent_role: str = "") -> None:
|
||||
"""Append one audit event. Never raises — audit logging must never break
|
||||
a chat turn, a permission decision, or a tool call."""
|
||||
try:
|
||||
now = datetime.now()
|
||||
event = {
|
||||
"ts": now.isoformat(timespec="seconds"),
|
||||
"kind": kind,
|
||||
"agent_role": agent_role or "",
|
||||
"name": name or "",
|
||||
"ok": bool(ok),
|
||||
"detail": (detail or "")[:2000], # bounded — never let a huge blob bloat the log
|
||||
"account": _identity_account,
|
||||
"role": _identity_role,
|
||||
"machine": _identity_machine,
|
||||
}
|
||||
AUDIT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
path = AUDIT_DIR / f"{now.strftime('%Y-%m-%d')}.jsonl"
|
||||
with path.open("a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(event, ensure_ascii=False) + "\n")
|
||||
_write_shared(event, now)
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
|
||||
def _write_shared(event: Dict[str, Any], now: datetime) -> None:
|
||||
"""Best-effort mirror of ``event`` into the shared cross-machine store —
|
||||
one file PER MACHINE per day, so no two machines ever write the same
|
||||
file. Never raises."""
|
||||
if not _identity_shared_dir or not _identity_machine:
|
||||
return
|
||||
try:
|
||||
shared = Path(_identity_shared_dir).expanduser() / "telemetry" / "audit"
|
||||
shared.mkdir(parents=True, exist_ok=True)
|
||||
path = shared / f"{_identity_machine}-{now.strftime('%Y-%m-%d')}.jsonl"
|
||||
with path.open("a", encoding="utf-8") as f:
|
||||
f.write(json.dumps(event, ensure_ascii=False) + "\n")
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
_logger.record(kind, name, ok, detail=detail, agent_role=agent_role)
|
||||
|
||||
|
||||
def load_events(start: Optional[date] = None, end: Optional[date] = None,
|
||||
@@ -91,25 +54,5 @@ def load_events(start: Optional[date] = None, end: Optional[date] = None,
|
||||
"""Events between ``start``/``end`` (inclusive; None = unbounded),
|
||||
optionally filtered to one ``kind`` — this IS how each Monitoring
|
||||
Dashboard panel gets its own slice of the same underlying log."""
|
||||
directory = directory or AUDIT_DIR
|
||||
if not directory.exists():
|
||||
return []
|
||||
events: List[Dict[str, Any]] = []
|
||||
for path in sorted(directory.glob("*.jsonl")):
|
||||
try:
|
||||
day = datetime.strptime(path.stem, "%Y-%m-%d").date()
|
||||
except ValueError:
|
||||
continue
|
||||
if (start and day < start) or (end and day > end):
|
||||
continue
|
||||
try:
|
||||
for line in path.read_text(encoding="utf-8").splitlines():
|
||||
if not line.strip():
|
||||
continue
|
||||
event = json.loads(line)
|
||||
if kind is not None and event.get("kind") != kind:
|
||||
continue
|
||||
events.append(event)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
continue
|
||||
return events
|
||||
events = _logger.load_events(start=start, end=end, kind=kind, directory=directory)
|
||||
return [e.to_dict() for e in events]
|
||||
|
||||
+6
-2
@@ -18,6 +18,8 @@ existing ``core/skills.py`` registry.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from ..infrastructure.persistence.json.atomic_json_file import AtomicJsonFile
|
||||
|
||||
import json
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from pathlib import Path
|
||||
@@ -233,7 +235,9 @@ def save_workflow(wf: Workflow, directory: Optional[Path] = None) -> Path:
|
||||
directory = directory or WORKFLOWS_DIR
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
path = directory / f"{wf.id}.json"
|
||||
path.write_text(json.dumps(workflow_to_dict(wf), ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
# Tiêu chí nghiệm thu A: mọi thao tác ghi tệp đi qua AtomicJsonFile. Trước
|
||||
# đây ghi thẳng, nên tắt máy giữa lúc lưu là mất luôn workflow.
|
||||
AtomicJsonFile(path).write(workflow_to_dict(wf))
|
||||
return path
|
||||
|
||||
|
||||
@@ -307,7 +311,7 @@ def save_custom_agent(agent: CustomAgent, directory: Optional[Path] = None) -> P
|
||||
directory = directory or AGENTS_DIR
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
path = directory / f"{agent.id}.json"
|
||||
path.write_text(json.dumps(agent_to_dict(agent), ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
AtomicJsonFile(path).write(agent_to_dict(agent))
|
||||
return path
|
||||
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ the run that is currently open.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from ..infrastructure.persistence.json.atomic_json_file import AtomicJsonFile
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional
|
||||
@@ -152,10 +154,10 @@ class Co4ERunManager(QObject):
|
||||
payload = {"runs": [h.to_record() for h in runs]}
|
||||
try:
|
||||
path.parent.mkdir(parents=True, exist_ok=True)
|
||||
tmp = path.with_suffix(".json.tmp")
|
||||
tmp.write_text(json.dumps(payload, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8")
|
||||
tmp.replace(path) # atomic — never leaves a half-written file
|
||||
# AtomicJsonFile thay cho tmp+replace tự viết: bản cũ thiếu fsync
|
||||
# (dữ liệu có thể còn trong bộ đệm khi mất điện) và dùng thẳng
|
||||
# Path.replace, vốn thỉnh thoảng bị Defender từ chối trên Windows.
|
||||
AtomicJsonFile(path).write(payload)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
+13
-3
@@ -32,6 +32,14 @@ _SYMBOL_CCY = {"₫": "VND", "vnd": "VND", "đ": "VND",
|
||||
|
||||
_DEFAULT_UNIT = "Million tokens"
|
||||
|
||||
# Flat USD/1M-token fallback rates used by turn_cost_usd() when a model isn't
|
||||
# in the price table. Owned here (not usage_tracker.DEFAULT_PRICING) so this
|
||||
# module never needs to import usage_tracker — usage_tracker imports this
|
||||
# module instead, keeping the dependency one-directional. Values match
|
||||
# usage_tracker.DEFAULT_PRICING's price_per_mtok_in_usd/out_usd exactly.
|
||||
_FALLBACK_RATE_IN_USD = 0.5
|
||||
_FALLBACK_RATE_OUT_USD = 1.5
|
||||
|
||||
|
||||
# ---- currency ------------------------------------------------------------
|
||||
def _rates(config) -> Dict[str, float]:
|
||||
@@ -138,9 +146,11 @@ def turn_cost_usd(model: str, in_tok: int, out_tok: int, config) -> float:
|
||||
switches models (a different model → its own row / rates)."""
|
||||
rates = usd_rates_for(model, config)
|
||||
if rates is None:
|
||||
from . import usage_tracker as ut
|
||||
p = {**ut.DEFAULT_PRICING, **((getattr(config, "data", {}) or {}).get("usage") or {})}
|
||||
rates = {"in": float(p["price_per_mtok_in_usd"]), "out": float(p["price_per_mtok_out_usd"])}
|
||||
usage = (getattr(config, "data", {}) or {}).get("usage") or {}
|
||||
rates = {
|
||||
"in": float(usage.get("price_per_mtok_in_usd", _FALLBACK_RATE_IN_USD)),
|
||||
"out": float(usage.get("price_per_mtok_out_usd", _FALLBACK_RATE_OUT_USD)),
|
||||
}
|
||||
return (in_tok or 0) / 1e6 * rates["in"] + (out_tok or 0) / 1e6 * rates["out"]
|
||||
|
||||
|
||||
|
||||
+69
-19
@@ -2,7 +2,9 @@
|
||||
|
||||
``execute_task`` dispatches by ``task_type`` to the app's existing engines:
|
||||
|
||||
- ``cowork`` → ``chat_agent.run_cowork`` (documents/answers, real files)
|
||||
- ``cowork`` → ``ConversationApplicationService`` (documents/answers, real
|
||||
files) — the same turn engine the interactive Cowork chat
|
||||
runs on since R04-T05
|
||||
- ``co4e_code`` → ``code_agent.run_code`` (code agent with file/command tools)
|
||||
- ``script`` → local subprocess with a timeout
|
||||
- ``flow`` → the task's own simple step list, run sequentially, each
|
||||
@@ -162,6 +164,30 @@ _TIMEOUT_NOTICE_TMPL = (
|
||||
)
|
||||
|
||||
|
||||
_UNATTENDED_PREFIX = (
|
||||
"This runs unattended (Schedule Task) — no one is watching live. Use "
|
||||
"update_plan to track your steps and keep it accurate: mark a step "
|
||||
"'error' (not silently skip it) if it genuinely can't be completed."
|
||||
)
|
||||
|
||||
|
||||
def _unattended_prompt(prompt: str, *, skill_text: str = "",
|
||||
agent_instructions: str = "") -> str:
|
||||
"""Assemble the user message an unattended run sends.
|
||||
|
||||
The order is load-bearing and used to be encoded as three successive
|
||||
rebindings of ``prompt``, each prepending its own block: the plan reminder
|
||||
must lead (it is the instruction that keeps a run without a human watching
|
||||
honest), then the chosen skill's rules, then the Admin agent's persona, and
|
||||
the task's own words last. Routing it through ``combine_instructions`` keeps
|
||||
that order in one readable expression and drops the absent blocks instead of
|
||||
leaving blank lines behind.
|
||||
"""
|
||||
from ..application.conversations.turn_runtime import combine_instructions
|
||||
|
||||
return combine_instructions(_UNATTENDED_PREFIX, skill_text, agent_instructions, prompt)
|
||||
|
||||
|
||||
def _cancel_with_timeout(cancel: CancelFn, timeout_sec: Optional[int]) -> Tuple[CancelFn, Callable[[], bool]]:
|
||||
"""Wrap ``cancel`` so it also fires once ``timeout_sec`` of wall-clock time
|
||||
elapses. ``timed_out()`` tells the caller whether THAT is why it stopped
|
||||
@@ -218,36 +244,29 @@ def _run_agent(ctx, task_type: str, prompt: str, out_dir: Path,
|
||||
# default, see state.build_provider_for). A legacy Admin-agent preset
|
||||
# (task.admin_agent_id), if still set on an older task, keeps working and
|
||||
# takes precedence — it pins the provider/model AND prepends instructions.
|
||||
agent_instructions = ""
|
||||
if admin_agent is not None:
|
||||
from .admin_agents import build_agent_provider
|
||||
|
||||
provider = build_agent_provider(ctx, admin_agent)
|
||||
agent_instructions = admin_agent.effective_prompt()
|
||||
if agent_instructions:
|
||||
prompt = f"{agent_instructions}\n\n{prompt}"
|
||||
elif provider_name or model:
|
||||
# An explicit per-task provider/model override.
|
||||
provider = ctx.build_provider_for(provider_name or None, model or None)
|
||||
else:
|
||||
# Neither overridden → the machine's own Settings default, exactly as before.
|
||||
provider = ctx.build_active_provider()
|
||||
# A chosen skill's instructions are prepended so this unattended run follows
|
||||
# A chosen skill's instructions are applied so this unattended run follows
|
||||
# them, mirroring how the interactive chat applies /skill.
|
||||
skill_text = ""
|
||||
if skill_slug:
|
||||
from .skills import skill_prefix_for
|
||||
|
||||
skill_text = skill_prefix_for(skill_slug)
|
||||
if skill_text:
|
||||
prompt = f"{skill_text}\n\n{prompt}"
|
||||
# This is an UNATTENDED run (no human watching to catch a half-finished
|
||||
# job) — push the agent to actually use the Plan checklist so completion
|
||||
# can be verified afterward, instead of just trusting "no exception".
|
||||
prompt = (
|
||||
"This runs unattended (Schedule Task) — no one is watching live. Use "
|
||||
"update_plan to track your steps and keep it accurate: mark a step "
|
||||
"'error' (not silently skip it) if it genuinely can't be completed.\n\n"
|
||||
f"{prompt}"
|
||||
)
|
||||
# Assemble reminder + skill + persona + the task's own words in one place
|
||||
# (see _unattended_prompt for why that order matters).
|
||||
prompt = _unattended_prompt(prompt, skill_text=skill_text,
|
||||
agent_instructions=agent_instructions)
|
||||
messages = [{"role": "user", "content": prompt}]
|
||||
session_id = new_session_id()
|
||||
project_id = project.project_id if project is not None else ""
|
||||
@@ -273,10 +292,41 @@ def _run_agent(ctx, task_type: str, prompt: str, out_dir: Path,
|
||||
watched_cancel, timed_out = _cancel_with_timeout(cancel, timeout_sec)
|
||||
try:
|
||||
if task_type == "cowork":
|
||||
from .chat_agent import run_cowork
|
||||
run_cowork(provider, messages, out_dir, emit_and_autosave, watched_cancel,
|
||||
security_config=ctx.config, agent_role=agent_roles.TASK,
|
||||
project_context=project_context)
|
||||
# R04-T05: the unattended run shares the interactive turn engine
|
||||
# instead of calling run_cowork itself, so there is exactly one place
|
||||
# where a turn's lifecycle is defined. Everything unattended-specific
|
||||
# stays here (the plan reminder above, the History autosave in
|
||||
# emit_and_autosave, the timeout notice below).
|
||||
from ..application.conversations.core_runtime_adapter import (
|
||||
build_cowork_conversation_service,
|
||||
legacy_event_sink,
|
||||
)
|
||||
from ..domain.agents.conversation_execution_request import (
|
||||
ConversationExecutionRequest,
|
||||
)
|
||||
|
||||
# No extra_tools/extra_executor and no permission gate: a scheduled
|
||||
# run gets no MCP connectors and nobody is there to approve a
|
||||
# command, which is exactly what run_cowork was called with.
|
||||
service = build_cowork_conversation_service(
|
||||
provider, out_dir, emit_and_autosave, title=title,
|
||||
project_context=project_context, security_config=ctx.config,
|
||||
agent_role=agent_roles.TASK,
|
||||
)
|
||||
request = ConversationExecutionRequest(
|
||||
# The artifact folder is named by the run id, which identifies
|
||||
# this attempt in the audit log.
|
||||
turn_id=out_dir.name or session_id, session_id=session_id,
|
||||
surface="task", title=title, project_id=project_id,
|
||||
prompt=prompt, output_dir=out_dir,
|
||||
agent_role=agent_roles.TASK, unattended=True,
|
||||
timeout_sec=timeout_sec,
|
||||
)
|
||||
# ``messages`` is handed over so the History autosave in
|
||||
# emit_and_autosave (and the final save in the finally block below)
|
||||
# keep reading the live conversation as it grows.
|
||||
service.execute(request, legacy_event_sink(emit_and_autosave),
|
||||
cancel=watched_cancel, messages=messages)
|
||||
else:
|
||||
from .code_agent import run_code
|
||||
limits, block_network = agent_security.sandbox_settings(ctx.config)
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Dựng câu nhắc cho AI phân tích mức dùng — R09-T02.
|
||||
|
||||
Chỉ sinh văn bản. Tách riêng vì đây là phần dễ đổi nhất (câu chữ, cột hiển
|
||||
thị) và không liên quan tới việc ghi nhận hay tính tiền.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import threading
|
||||
from datetime import date, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from ..config import CONFIG_DIR
|
||||
from . import model_pricing as mp
|
||||
from .usage_periods import period_breakdown, period_range_label
|
||||
|
||||
_AI_ANALYSIS_HEADERS = {
|
||||
"vi": ("Nhận xét thói quen", "Cách viết prompt tiết kiệm hơn", "Hành động giảm token"),
|
||||
"en": ("Usage habits", "Writing more efficient prompts", "Actions to cut token usage"),
|
||||
"ja": ("利用傾向", "より効率的なプロンプトの書き方", "トークン削減のためのアクション"),
|
||||
}
|
||||
|
||||
def build_ai_analysis_prompt(summary: Dict[str, Any], language: str = "vi") -> str:
|
||||
"""The prompt sent to the model for '✨ AI analyze my usage': aggregated
|
||||
numbers only — never raw prompt contents — asking for concrete habits
|
||||
feedback and token-saving recommendations, in the CURRENTLY SELECTED
|
||||
display language (headers included — not just the model's free-text reply,
|
||||
which would otherwise leave the section titles in Vietnamese regardless of
|
||||
the app's language setting)."""
|
||||
lang_names = {"vi": "Vietnamese", "ja": "Japanese", "en": "English"}
|
||||
h1, h2, h3 = _AI_ANALYSIS_HEADERS.get(language, _AI_ANALYSIS_HEADERS["vi"])
|
||||
top = "\n".join(f"- {label}: {tok:,} tokens"
|
||||
for label, tok in summary.get("top_labels", []))
|
||||
by_source = ", ".join(f"{k}={v:,}" for k, v in summary.get("by_source", []))
|
||||
return (
|
||||
"You are a token-efficiency coach for an AI desktop app (chat tabs + "
|
||||
"scheduled agent tasks). Analyze this usage summary and give the user "
|
||||
"practical advice, replying in "
|
||||
f"{lang_names.get(language, 'Vietnamese')}.\n\n"
|
||||
f"Period stats: {summary.get('turns', 0)} turns, "
|
||||
f"input={summary.get('in', 0):,} tokens, output={summary.get('out', 0):,}, "
|
||||
f"cache={summary.get('cache', 0):,}, "
|
||||
f"avg per prompt={summary.get('avg_per_turn', 0):,}.\n"
|
||||
f"Top consumers:\n{top or '- (none)'}\n"
|
||||
f"By area: {by_source or '(none)'}\n"
|
||||
f"Busiest day: {summary.get('busiest_day')} · busiest hour: {summary.get('busiest_hour')}\n\n"
|
||||
"Reply with EXACTLY these 3 short sections, in markdown, using THESE "
|
||||
f"section headers verbatim (already in {lang_names.get(language, 'Vietnamese')}):\n"
|
||||
f"1. **{h1}** — 2-3 bullet points about the usage pattern.\n"
|
||||
f"2. **{h2}** — 3 concrete prompt-writing tips "
|
||||
"tailored to the numbers above (e.g. long inputs → attach less / summarize "
|
||||
"first; many small turns → batch questions).\n"
|
||||
f"3. **{h3}** — 2-3 app-level actions (compact history, "
|
||||
"smaller model for simple tasks, reuse task outputs instead of re-asking).\n"
|
||||
"Keep the whole reply under 250 words."
|
||||
)
|
||||
@@ -0,0 +1,101 @@
|
||||
"""Bảng giá và quy đổi token thành tiền — R09-T02.
|
||||
|
||||
Tách khỏi ``usage_tracker.py``: ghi nhận mức dùng và tính tiền là hai việc
|
||||
khác nhau. Bảng giá đổi theo nhà cung cấp, cách ghi nhận thì không.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import threading
|
||||
from datetime import date, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from ..config import CONFIG_DIR
|
||||
from . import model_pricing as mp
|
||||
|
||||
DEFAULT_PRICING = {
|
||||
"price_per_mtok_in_usd": 0.5, # USD per 1M input tokens (flat fallback rate)
|
||||
"price_per_mtok_out_usd": 1.5, # USD per 1M output tokens
|
||||
"price_per_mtok_cache_usd": 0.1, # USD per 1M cached tokens
|
||||
"currency": "USD", # display currency: USD | VND | JPY
|
||||
"usd_to_vnd": 25000.0,
|
||||
"usd_to_jpy": 150.0,
|
||||
# Per-model price table (USD / 1M tokens): {model: {"in","out","cache"}}.
|
||||
# Events whose model has an entry are costed with ITS rates; everything
|
||||
# else falls back to the flat price_per_mtok_* rates above. Edited in the
|
||||
# Monitoring Overview's pricing table.
|
||||
"model_prices": {},
|
||||
# Reference URL of the price list the table was filled from (set in
|
||||
# Settings; shown as a link beside the table — informational only, the
|
||||
# app never scrapes it).
|
||||
"pricing_url": "",
|
||||
}
|
||||
|
||||
_CURRENCY_FMT = {"USD": ("$", 4), "VND": ("₫", 0), "JPY": ("¥", 1)}
|
||||
|
||||
SUPPORTED_CURRENCIES = tuple(_CURRENCY_FMT)
|
||||
|
||||
def cost_usd(summary: Dict[str, Any], pricing: Dict[str, Any]) -> Dict[str, float]:
|
||||
p = {**DEFAULT_PRICING, **(pricing or {})}
|
||||
return {
|
||||
"in": summary.get("in", 0) / 1e6 * float(p["price_per_mtok_in_usd"]),
|
||||
"out": summary.get("out", 0) / 1e6 * float(p["price_per_mtok_out_usd"]),
|
||||
"cache": summary.get("cache", 0) / 1e6 * float(p["price_per_mtok_cache_usd"]),
|
||||
}
|
||||
|
||||
def cost_usd_events(events: List[Dict[str, Any]], pricing: Dict[str, Any]) -> Dict[str, float]:
|
||||
"""Per-bucket USD cost computed EVENT BY EVENT so the per-model price
|
||||
table applies: an event whose ``model`` has an entry in
|
||||
``pricing["model_prices"]`` is costed with that model's own rates; any
|
||||
other event uses the flat ``price_per_mtok_*`` rates. With an empty
|
||||
table this equals ``cost_usd(summarize(events), pricing)`` exactly."""
|
||||
p = {**DEFAULT_PRICING, **(pricing or {})}
|
||||
table = p.get("model_prices") or {}
|
||||
flat = {"in": float(p["price_per_mtok_in_usd"]),
|
||||
"out": float(p["price_per_mtok_out_usd"]),
|
||||
"cache": float(p["price_per_mtok_cache_usd"])}
|
||||
out = {"in": 0.0, "out": 0.0, "cache": 0.0}
|
||||
for e in events:
|
||||
rates = table.get(e.get("model", "")) or {}
|
||||
for bucket in ("in", "out", "cache"):
|
||||
try:
|
||||
rate = float(rates.get(bucket, flat[bucket]))
|
||||
except (TypeError, ValueError):
|
||||
rate = flat[bucket]
|
||||
out[bucket] += e.get(bucket, 0) / 1e6 * rate
|
||||
return out
|
||||
|
||||
def format_cost(usd: float, pricing: Dict[str, Any], digits: Optional[int] = None) -> str:
|
||||
"""Format a USD amount in the display currency. ``digits`` caps the number
|
||||
of decimal places (e.g. ``digits=2`` for the Total cost / Budget cards, so
|
||||
USD shows $1.23 not the default up-to-4 $1.2345) — never ADDS decimals to a
|
||||
currency that uses fewer (VND stays whole, JPY one place)."""
|
||||
p = {**DEFAULT_PRICING, **(pricing or {})}
|
||||
cur = p.get("currency", "USD")
|
||||
rate = {"USD": 1.0, "VND": float(p["usd_to_vnd"]), "JPY": float(p["usd_to_jpy"])}.get(cur, 1.0)
|
||||
symbol, cur_digits = _CURRENCY_FMT.get(cur, ("$", 2))
|
||||
if digits is not None:
|
||||
cur_digits = min(cur_digits, digits)
|
||||
value = usd * rate
|
||||
return f"{symbol}{value:,.{cur_digits}f}"
|
||||
|
||||
def format_cost_compact(usd: float, pricing: Dict[str, Any]) -> str:
|
||||
"""Compact cost format for the Dashboard chart's y-axis/endpoint labels —
|
||||
always 2 decimals (not format_cost's up-to-4 for USD) and abbreviated with
|
||||
K/M above 1,000/1,000,000, same convention as ``fmt_tokens``. The chart's
|
||||
y-axis label box is narrow; the longer full-precision string used to
|
||||
overflow it, visually clipping/obscuring the leading currency symbol."""
|
||||
p = {**DEFAULT_PRICING, **(pricing or {})}
|
||||
cur = p.get("currency", "USD")
|
||||
rate = {"USD": 1.0, "VND": float(p["usd_to_vnd"]), "JPY": float(p["usd_to_jpy"])}.get(cur, 1.0)
|
||||
symbol, _digits = _CURRENCY_FMT.get(cur, ("$", 2))
|
||||
value = usd * rate
|
||||
sign = "-" if value < 0 else ""
|
||||
value = abs(value)
|
||||
if value >= 1_000_000:
|
||||
body = f"{value / 1_000_000:,.2f}M"
|
||||
elif value >= 1_000:
|
||||
body = f"{value / 1_000:,.2f}K"
|
||||
else:
|
||||
body = f"{value:,.2f}"
|
||||
return f"{sign}{symbol}{body}"
|
||||
@@ -0,0 +1,144 @@
|
||||
"""Gộp mức dùng theo khoảng thời gian — R09-T02.
|
||||
|
||||
Ngày / tuần / tháng / quý: ranh giới khoảng, nhãn hiển thị, chuỗi số vẽ biểu
|
||||
đồ. Thuần tính toán trên danh sách sự kiện, không đụng đĩa.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import threading
|
||||
from datetime import date, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
from ..config import CONFIG_DIR
|
||||
from . import model_pricing as mp
|
||||
from .usage_cost import cost_usd_events
|
||||
|
||||
def bucketed_series(events: List[Dict[str, Any]], granularity: str = "day",
|
||||
pricing: Dict[str, Any] = None, last: int = None) -> List[tuple]:
|
||||
"""Group usage events into time buckets → ordered ``[(label, tokens, cost_usd)]``.
|
||||
|
||||
``granularity``: ``day`` (YYYY-MM-DD) · ``month`` (YYYY-MM) · ``year`` (YYYY).
|
||||
``last`` keeps only the most recent N buckets (for the dashboard chart)."""
|
||||
from collections import OrderedDict
|
||||
pricing = pricing or {}
|
||||
|
||||
def _key(ts: Any) -> str:
|
||||
s = str(ts or "")[:10]
|
||||
if granularity == "year":
|
||||
return s[:4]
|
||||
if granularity == "month":
|
||||
return s[:7]
|
||||
return s
|
||||
|
||||
buckets: "OrderedDict[str, List[Dict[str, Any]]]" = OrderedDict()
|
||||
for e in sorted(events, key=lambda ev: str(ev.get("ts", ""))):
|
||||
k = _key(e.get("ts"))
|
||||
if k:
|
||||
buckets.setdefault(k, []).append(e)
|
||||
out = []
|
||||
for k, evs in buckets.items():
|
||||
tokens = sum(int(e.get("in", 0) or 0) + int(e.get("out", 0) or 0)
|
||||
+ int(e.get("cache", 0) or 0) for e in evs)
|
||||
cost = sum(cost_usd_events(evs, pricing).values())
|
||||
out.append((k, tokens, cost))
|
||||
if last and len(out) > last:
|
||||
out = out[-last:]
|
||||
return out
|
||||
|
||||
def period_bounds(gran: str, offset: int, today: Optional[date] = None) -> tuple:
|
||||
"""[start, end) dates of the period ``offset`` periods from the current one
|
||||
(0 = current, -1 = the previous week/month/year). Weeks run Mon→Sun."""
|
||||
from datetime import timedelta
|
||||
today = today or date.today()
|
||||
if gran == "week":
|
||||
monday = today - timedelta(days=today.weekday()) # Monday of this week
|
||||
start = monday + timedelta(weeks=offset)
|
||||
return start, start + timedelta(days=7)
|
||||
if gran == "year":
|
||||
y = today.year + offset
|
||||
return date(y, 1, 1), date(y + 1, 1, 1)
|
||||
# month (default)
|
||||
base = today.year * 12 + (today.month - 1) + offset
|
||||
y, m = divmod(base, 12)
|
||||
y2, m2 = divmod(base + 1, 12)
|
||||
return date(y, m + 1, 1), date(y2, m2 + 1, 1)
|
||||
|
||||
def _period_label(gran: str, start: date) -> str:
|
||||
if gran == "week":
|
||||
return start.isoformat() # the week's Monday (YYYY-MM-DD)
|
||||
if gran == "year":
|
||||
return str(start.year)
|
||||
return start.strftime("%Y-%m")
|
||||
|
||||
def _sum_between(events: List[Dict[str, Any]], start: date, end: date,
|
||||
pricing: Dict[str, Any]) -> tuple:
|
||||
lo, hi = start.isoformat(), end.isoformat()
|
||||
evs = [e for e in events if lo <= str(e.get("ts", ""))[:10] < hi]
|
||||
tokens = sum(int(e.get("in", 0) or 0) + int(e.get("out", 0) or 0)
|
||||
+ int(e.get("cache", 0) or 0) for e in evs)
|
||||
cost = sum(cost_usd_events(evs, pricing).values()) if evs else 0.0
|
||||
return tokens, cost
|
||||
|
||||
def period_totals(events: List[Dict[str, Any]], gran: str, pricing: Dict[str, Any],
|
||||
offset: int = 0, today: Optional[date] = None) -> tuple:
|
||||
"""(tokens, cost_usd) for the single period ``offset`` periods from now."""
|
||||
start, end = period_bounds(gran, offset, today)
|
||||
return _sum_between(events, start, end, pricing)
|
||||
|
||||
def period_window(events: List[Dict[str, Any]], gran: str, pricing: Dict[str, Any],
|
||||
count: int, offset: int = 0, today: Optional[date] = None) -> List[tuple]:
|
||||
"""``count`` consecutive, ZERO-FILLED periods ending at (current + offset),
|
||||
ordered oldest→newest → ``[(label, tokens, cost_usd)]``. ``offset`` (≤ 0)
|
||||
pages the window into the past for the Dashboard's prev/next navigation."""
|
||||
out = []
|
||||
for i in range(count - 1, -1, -1):
|
||||
start, end = period_bounds(gran, offset - i, today)
|
||||
tok, cost = _sum_between(events, start, end, pricing)
|
||||
out.append((_period_label(gran, start), tok, cost))
|
||||
return out
|
||||
|
||||
def period_breakdown(events: List[Dict[str, Any]], gran: str, pricing: Dict[str, Any],
|
||||
offset: int = 0, today: Optional[date] = None) -> List[tuple]:
|
||||
"""Break the SELECTED period (``offset`` periods from now) into its sub-parts
|
||||
→ ``[(label, tokens, cost_usd)]``:
|
||||
· week → 7 days Mon→Sun (label ``MM/DD``)
|
||||
· month → weeks W1…Wn (7-day chunks from the 1st)
|
||||
· year → 12 months (label ``01``…``12``)."""
|
||||
from datetime import timedelta
|
||||
start, end = period_bounds(gran, offset, today)
|
||||
out = []
|
||||
if gran == "week":
|
||||
for i in range(7):
|
||||
d = start + timedelta(days=i)
|
||||
tok, cost = _sum_between(events, d, d + timedelta(days=1), pricing)
|
||||
out.append((d.strftime("%m/%d"), tok, cost))
|
||||
elif gran == "year":
|
||||
for m in range(1, 13):
|
||||
ms = date(start.year, m, 1)
|
||||
me = date(start.year + 1, 1, 1) if m == 12 else date(start.year, m + 1, 1)
|
||||
tok, cost = _sum_between(events, ms, me, pricing)
|
||||
out.append((f"{m:02d}", tok, cost))
|
||||
else: # month → weeks W1..Wn
|
||||
ndays = (end - start).days
|
||||
wk, day = 1, 1
|
||||
while day <= ndays:
|
||||
ws = date(start.year, start.month, day)
|
||||
we = date(start.year, start.month, day + 7) if day + 7 <= ndays else end
|
||||
tok, cost = _sum_between(events, ws, we, pricing)
|
||||
out.append((f"W{wk}", tok, cost))
|
||||
wk += 1
|
||||
day += 7
|
||||
return out
|
||||
|
||||
def period_range_label(gran: str, offset: int, today: Optional[date] = None) -> str:
|
||||
"""Human label for the selected period (shown in the Dashboard header) —
|
||||
week → MM/DD – MM/DD, month → YYYY/MM, year → YYYY."""
|
||||
from datetime import timedelta
|
||||
start, end = period_bounds(gran, offset, today)
|
||||
if gran == "week":
|
||||
last_day = end - timedelta(days=1)
|
||||
return f"{start.strftime('%m/%d')} – {last_day.strftime('%m/%d')}"
|
||||
if gran == "year":
|
||||
return str(start.year)
|
||||
return start.strftime("%Y/%m")
|
||||
+24
-241
@@ -12,6 +12,17 @@ The turn's source/label is set by the caller ON THE WORKER THREAD via
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
# Giữ đường vào cũ: nhiều nơi import mấy tên này thẳng từ usage_tracker.
|
||||
from .usage_ai_report import build_ai_analysis_prompt # noqa: F401
|
||||
from .usage_cost import ( # noqa: F401
|
||||
DEFAULT_PRICING, SUPPORTED_CURRENCIES, cost_usd, cost_usd_events,
|
||||
format_cost, format_cost_compact,
|
||||
)
|
||||
from .usage_periods import ( # noqa: F401
|
||||
bucketed_series, period_bounds, period_breakdown, period_range_label,
|
||||
period_totals, period_window,
|
||||
)
|
||||
|
||||
import json
|
||||
import threading
|
||||
from datetime import date, datetime
|
||||
@@ -19,6 +30,7 @@ from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from ..config import CONFIG_DIR
|
||||
from . import model_pricing as mp
|
||||
|
||||
USAGE_DIR = CONFIG_DIR / "usage"
|
||||
|
||||
@@ -49,6 +61,18 @@ def set_context(source: str, label: str = "") -> None:
|
||||
_local.label = label
|
||||
|
||||
|
||||
def current_context() -> tuple:
|
||||
"""The ``(source, label)`` currently tagged on THIS thread.
|
||||
|
||||
Public counterpart to :func:`set_context`, added for
|
||||
``infrastructure/telemetry/usage_sink.py``: a subscriber that needs to
|
||||
attribute one event to a different surface must be able to save the
|
||||
caller's context and put it back afterwards, instead of leaving the worker
|
||||
thread permanently retagged.
|
||||
"""
|
||||
return getattr(_local, "source", "") or "", getattr(_local, "label", "") or ""
|
||||
|
||||
|
||||
# ---- per-thread usage accumulator -----------------------------------------
|
||||
# A step/run that wants to know its OWN token/cost (not the all-time file total)
|
||||
# calls begin_accumulation(), reads accumulated() before/after a unit of work,
|
||||
@@ -191,198 +215,30 @@ def summarize(events: List[Dict[str, Any]]) -> Dict[str, Any]:
|
||||
|
||||
|
||||
# ---- cost ------------------------------------------------------------------
|
||||
DEFAULT_PRICING = {
|
||||
"price_per_mtok_in_usd": 0.5, # USD per 1M input tokens (flat fallback rate)
|
||||
"price_per_mtok_out_usd": 1.5, # USD per 1M output tokens
|
||||
"price_per_mtok_cache_usd": 0.1, # USD per 1M cached tokens
|
||||
"currency": "USD", # display currency: USD | VND | JPY
|
||||
"usd_to_vnd": 25000.0,
|
||||
"usd_to_jpy": 150.0,
|
||||
# Per-model price table (USD / 1M tokens): {model: {"in","out","cache"}}.
|
||||
# Events whose model has an entry are costed with ITS rates; everything
|
||||
# else falls back to the flat price_per_mtok_* rates above. Edited in the
|
||||
# Monitoring Overview's pricing table.
|
||||
"model_prices": {},
|
||||
# Reference URL of the price list the table was filled from (set in
|
||||
# Settings; shown as a link beside the table — informational only, the
|
||||
# app never scrapes it).
|
||||
"pricing_url": "",
|
||||
}
|
||||
|
||||
_CURRENCY_FMT = {"USD": ("$", 4), "VND": ("₫", 0), "JPY": ("¥", 1)}
|
||||
|
||||
# Currencies the display picker offers — exactly the ones format_cost() can
|
||||
# actually convert to (symbol/precision above + a usd_to_* rate below).
|
||||
SUPPORTED_CURRENCIES = tuple(_CURRENCY_FMT)
|
||||
|
||||
|
||||
def cost_usd(summary: Dict[str, Any], pricing: Dict[str, Any]) -> Dict[str, float]:
|
||||
p = {**DEFAULT_PRICING, **(pricing or {})}
|
||||
return {
|
||||
"in": summary.get("in", 0) / 1e6 * float(p["price_per_mtok_in_usd"]),
|
||||
"out": summary.get("out", 0) / 1e6 * float(p["price_per_mtok_out_usd"]),
|
||||
"cache": summary.get("cache", 0) / 1e6 * float(p["price_per_mtok_cache_usd"]),
|
||||
}
|
||||
|
||||
|
||||
def cost_usd_events(events: List[Dict[str, Any]], pricing: Dict[str, Any]) -> Dict[str, float]:
|
||||
"""Per-bucket USD cost computed EVENT BY EVENT so the per-model price
|
||||
table applies: an event whose ``model`` has an entry in
|
||||
``pricing["model_prices"]`` is costed with that model's own rates; any
|
||||
other event uses the flat ``price_per_mtok_*`` rates. With an empty
|
||||
table this equals ``cost_usd(summarize(events), pricing)`` exactly."""
|
||||
p = {**DEFAULT_PRICING, **(pricing or {})}
|
||||
table = p.get("model_prices") or {}
|
||||
flat = {"in": float(p["price_per_mtok_in_usd"]),
|
||||
"out": float(p["price_per_mtok_out_usd"]),
|
||||
"cache": float(p["price_per_mtok_cache_usd"])}
|
||||
out = {"in": 0.0, "out": 0.0, "cache": 0.0}
|
||||
for e in events:
|
||||
rates = table.get(e.get("model", "")) or {}
|
||||
for bucket in ("in", "out", "cache"):
|
||||
try:
|
||||
rate = float(rates.get(bucket, flat[bucket]))
|
||||
except (TypeError, ValueError):
|
||||
rate = flat[bucket]
|
||||
out[bucket] += e.get(bucket, 0) / 1e6 * rate
|
||||
return out
|
||||
|
||||
|
||||
def bucketed_series(events: List[Dict[str, Any]], granularity: str = "day",
|
||||
pricing: Dict[str, Any] = None, last: int = None) -> List[tuple]:
|
||||
"""Group usage events into time buckets → ordered ``[(label, tokens, cost_usd)]``.
|
||||
|
||||
``granularity``: ``day`` (YYYY-MM-DD) · ``month`` (YYYY-MM) · ``year`` (YYYY).
|
||||
``last`` keeps only the most recent N buckets (for the dashboard chart)."""
|
||||
from collections import OrderedDict
|
||||
pricing = pricing or {}
|
||||
|
||||
def _key(ts: Any) -> str:
|
||||
s = str(ts or "")[:10]
|
||||
if granularity == "year":
|
||||
return s[:4]
|
||||
if granularity == "month":
|
||||
return s[:7]
|
||||
return s
|
||||
|
||||
buckets: "OrderedDict[str, List[Dict[str, Any]]]" = OrderedDict()
|
||||
for e in sorted(events, key=lambda ev: str(ev.get("ts", ""))):
|
||||
k = _key(e.get("ts"))
|
||||
if k:
|
||||
buckets.setdefault(k, []).append(e)
|
||||
out = []
|
||||
for k, evs in buckets.items():
|
||||
tokens = sum(int(e.get("in", 0) or 0) + int(e.get("out", 0) or 0)
|
||||
+ int(e.get("cache", 0) or 0) for e in evs)
|
||||
cost = sum(cost_usd_events(evs, pricing).values())
|
||||
out.append((k, tokens, cost))
|
||||
if last and len(out) > last:
|
||||
out = out[-last:]
|
||||
return out
|
||||
|
||||
|
||||
def period_bounds(gran: str, offset: int, today: Optional[date] = None) -> tuple:
|
||||
"""[start, end) dates of the period ``offset`` periods from the current one
|
||||
(0 = current, -1 = the previous week/month/year). Weeks run Mon→Sun."""
|
||||
from datetime import timedelta
|
||||
today = today or date.today()
|
||||
if gran == "week":
|
||||
monday = today - timedelta(days=today.weekday()) # Monday of this week
|
||||
start = monday + timedelta(weeks=offset)
|
||||
return start, start + timedelta(days=7)
|
||||
if gran == "year":
|
||||
y = today.year + offset
|
||||
return date(y, 1, 1), date(y + 1, 1, 1)
|
||||
# month (default)
|
||||
base = today.year * 12 + (today.month - 1) + offset
|
||||
y, m = divmod(base, 12)
|
||||
y2, m2 = divmod(base + 1, 12)
|
||||
return date(y, m + 1, 1), date(y2, m2 + 1, 1)
|
||||
|
||||
|
||||
def _period_label(gran: str, start: date) -> str:
|
||||
if gran == "week":
|
||||
return start.isoformat() # the week's Monday (YYYY-MM-DD)
|
||||
if gran == "year":
|
||||
return str(start.year)
|
||||
return start.strftime("%Y-%m")
|
||||
|
||||
|
||||
def _sum_between(events: List[Dict[str, Any]], start: date, end: date,
|
||||
pricing: Dict[str, Any]) -> tuple:
|
||||
lo, hi = start.isoformat(), end.isoformat()
|
||||
evs = [e for e in events if lo <= str(e.get("ts", ""))[:10] < hi]
|
||||
tokens = sum(int(e.get("in", 0) or 0) + int(e.get("out", 0) or 0)
|
||||
+ int(e.get("cache", 0) or 0) for e in evs)
|
||||
cost = sum(cost_usd_events(evs, pricing).values()) if evs else 0.0
|
||||
return tokens, cost
|
||||
|
||||
|
||||
def period_totals(events: List[Dict[str, Any]], gran: str, pricing: Dict[str, Any],
|
||||
offset: int = 0, today: Optional[date] = None) -> tuple:
|
||||
"""(tokens, cost_usd) for the single period ``offset`` periods from now."""
|
||||
start, end = period_bounds(gran, offset, today)
|
||||
return _sum_between(events, start, end, pricing)
|
||||
|
||||
|
||||
def period_window(events: List[Dict[str, Any]], gran: str, pricing: Dict[str, Any],
|
||||
count: int, offset: int = 0, today: Optional[date] = None) -> List[tuple]:
|
||||
"""``count`` consecutive, ZERO-FILLED periods ending at (current + offset),
|
||||
ordered oldest→newest → ``[(label, tokens, cost_usd)]``. ``offset`` (≤ 0)
|
||||
pages the window into the past for the Dashboard's prev/next navigation."""
|
||||
out = []
|
||||
for i in range(count - 1, -1, -1):
|
||||
start, end = period_bounds(gran, offset - i, today)
|
||||
tok, cost = _sum_between(events, start, end, pricing)
|
||||
out.append((_period_label(gran, start), tok, cost))
|
||||
return out
|
||||
|
||||
|
||||
def period_breakdown(events: List[Dict[str, Any]], gran: str, pricing: Dict[str, Any],
|
||||
offset: int = 0, today: Optional[date] = None) -> List[tuple]:
|
||||
"""Break the SELECTED period (``offset`` periods from now) into its sub-parts
|
||||
→ ``[(label, tokens, cost_usd)]``:
|
||||
· week → 7 days Mon→Sun (label ``MM/DD``)
|
||||
· month → weeks W1…Wn (7-day chunks from the 1st)
|
||||
· year → 12 months (label ``01``…``12``)."""
|
||||
from datetime import timedelta
|
||||
start, end = period_bounds(gran, offset, today)
|
||||
out = []
|
||||
if gran == "week":
|
||||
for i in range(7):
|
||||
d = start + timedelta(days=i)
|
||||
tok, cost = _sum_between(events, d, d + timedelta(days=1), pricing)
|
||||
out.append((d.strftime("%m/%d"), tok, cost))
|
||||
elif gran == "year":
|
||||
for m in range(1, 13):
|
||||
ms = date(start.year, m, 1)
|
||||
me = date(start.year + 1, 1, 1) if m == 12 else date(start.year, m + 1, 1)
|
||||
tok, cost = _sum_between(events, ms, me, pricing)
|
||||
out.append((f"{m:02d}", tok, cost))
|
||||
else: # month → weeks W1..Wn
|
||||
ndays = (end - start).days
|
||||
wk, day = 1, 1
|
||||
while day <= ndays:
|
||||
ws = date(start.year, start.month, day)
|
||||
we = date(start.year, start.month, day + 7) if day + 7 <= ndays else end
|
||||
tok, cost = _sum_between(events, ws, we, pricing)
|
||||
out.append((f"W{wk}", tok, cost))
|
||||
wk += 1
|
||||
day += 7
|
||||
return out
|
||||
|
||||
|
||||
def period_range_label(gran: str, offset: int, today: Optional[date] = None) -> str:
|
||||
"""Human label for the selected period (shown in the Dashboard header) —
|
||||
week → MM/DD – MM/DD, month → YYYY/MM, year → YYYY."""
|
||||
from datetime import timedelta
|
||||
start, end = period_bounds(gran, offset, today)
|
||||
if gran == "week":
|
||||
last_day = end - timedelta(days=1)
|
||||
return f"{start.strftime('%m/%d')} – {last_day.strftime('%m/%d')}"
|
||||
if gran == "year":
|
||||
return str(start.year)
|
||||
return start.strftime("%Y/%m")
|
||||
|
||||
|
||||
def set_budget(config, amount: float, currency: Optional[str] = None) -> None:
|
||||
@@ -397,7 +253,6 @@ def set_budget(config, amount: float, currency: Optional[str] = None) -> None:
|
||||
order and ``budget_set_at`` only has 1-second resolution, so a timestamp
|
||||
cutoff could mis-include/exclude an event recorded in that same second —
|
||||
the count baseline is exact regardless of timing."""
|
||||
from . import model_pricing as mp
|
||||
usage = config.data.setdefault("usage", {})
|
||||
ccy = (currency or usage.get("currency") or "USD").upper()
|
||||
usage["budget_amount_usd"] = mp.convert(float(amount or 0), ccy, "USD", config)
|
||||
@@ -442,83 +297,11 @@ def budget_status(config) -> Optional[Dict[str, Any]]:
|
||||
}
|
||||
|
||||
|
||||
def format_cost(usd: float, pricing: Dict[str, Any], digits: Optional[int] = None) -> str:
|
||||
"""Format a USD amount in the display currency. ``digits`` caps the number
|
||||
of decimal places (e.g. ``digits=2`` for the Total cost / Budget cards, so
|
||||
USD shows $1.23 not the default up-to-4 $1.2345) — never ADDS decimals to a
|
||||
currency that uses fewer (VND stays whole, JPY one place)."""
|
||||
p = {**DEFAULT_PRICING, **(pricing or {})}
|
||||
cur = p.get("currency", "USD")
|
||||
rate = {"USD": 1.0, "VND": float(p["usd_to_vnd"]), "JPY": float(p["usd_to_jpy"])}.get(cur, 1.0)
|
||||
symbol, cur_digits = _CURRENCY_FMT.get(cur, ("$", 2))
|
||||
if digits is not None:
|
||||
cur_digits = min(cur_digits, digits)
|
||||
value = usd * rate
|
||||
return f"{symbol}{value:,.{cur_digits}f}"
|
||||
|
||||
|
||||
def format_cost_compact(usd: float, pricing: Dict[str, Any]) -> str:
|
||||
"""Compact cost format for the Dashboard chart's y-axis/endpoint labels —
|
||||
always 2 decimals (not format_cost's up-to-4 for USD) and abbreviated with
|
||||
K/M above 1,000/1,000,000, same convention as ``fmt_tokens``. The chart's
|
||||
y-axis label box is narrow; the longer full-precision string used to
|
||||
overflow it, visually clipping/obscuring the leading currency symbol."""
|
||||
p = {**DEFAULT_PRICING, **(pricing or {})}
|
||||
cur = p.get("currency", "USD")
|
||||
rate = {"USD": 1.0, "VND": float(p["usd_to_vnd"]), "JPY": float(p["usd_to_jpy"])}.get(cur, 1.0)
|
||||
symbol, _digits = _CURRENCY_FMT.get(cur, ("$", 2))
|
||||
value = usd * rate
|
||||
sign = "-" if value < 0 else ""
|
||||
value = abs(value)
|
||||
if value >= 1_000_000:
|
||||
body = f"{value / 1_000_000:,.2f}M"
|
||||
elif value >= 1_000:
|
||||
body = f"{value / 1_000:,.2f}K"
|
||||
else:
|
||||
body = f"{value:,.2f}"
|
||||
return f"{sign}{symbol}{body}"
|
||||
|
||||
|
||||
_AI_ANALYSIS_HEADERS = {
|
||||
"vi": ("Nhận xét thói quen", "Cách viết prompt tiết kiệm hơn", "Hành động giảm token"),
|
||||
"en": ("Usage habits", "Writing more efficient prompts", "Actions to cut token usage"),
|
||||
"ja": ("利用傾向", "より効率的なプロンプトの書き方", "トークン削減のためのアクション"),
|
||||
}
|
||||
|
||||
|
||||
def build_ai_analysis_prompt(summary: Dict[str, Any], language: str = "vi") -> str:
|
||||
"""The prompt sent to the model for '✨ AI analyze my usage': aggregated
|
||||
numbers only — never raw prompt contents — asking for concrete habits
|
||||
feedback and token-saving recommendations, in the CURRENTLY SELECTED
|
||||
display language (headers included — not just the model's free-text reply,
|
||||
which would otherwise leave the section titles in Vietnamese regardless of
|
||||
the app's language setting)."""
|
||||
lang_names = {"vi": "Vietnamese", "ja": "Japanese", "en": "English"}
|
||||
h1, h2, h3 = _AI_ANALYSIS_HEADERS.get(language, _AI_ANALYSIS_HEADERS["vi"])
|
||||
top = "\n".join(f"- {label}: {tok:,} tokens"
|
||||
for label, tok in summary.get("top_labels", []))
|
||||
by_source = ", ".join(f"{k}={v:,}" for k, v in summary.get("by_source", []))
|
||||
return (
|
||||
"You are a token-efficiency coach for an AI desktop app (chat tabs + "
|
||||
"scheduled agent tasks). Analyze this usage summary and give the user "
|
||||
"practical advice, replying in "
|
||||
f"{lang_names.get(language, 'Vietnamese')}.\n\n"
|
||||
f"Period stats: {summary.get('turns', 0)} turns, "
|
||||
f"input={summary.get('in', 0):,} tokens, output={summary.get('out', 0):,}, "
|
||||
f"cache={summary.get('cache', 0):,}, "
|
||||
f"avg per prompt={summary.get('avg_per_turn', 0):,}.\n"
|
||||
f"Top consumers:\n{top or '- (none)'}\n"
|
||||
f"By area: {by_source or '(none)'}\n"
|
||||
f"Busiest day: {summary.get('busiest_day')} · busiest hour: {summary.get('busiest_hour')}\n\n"
|
||||
"Reply with EXACTLY these 3 short sections, in markdown, using THESE "
|
||||
f"section headers verbatim (already in {lang_names.get(language, 'Vietnamese')}):\n"
|
||||
f"1. **{h1}** — 2-3 bullet points about the usage pattern.\n"
|
||||
f"2. **{h2}** — 3 concrete prompt-writing tips "
|
||||
"tailored to the numbers above (e.g. long inputs → attach less / summarize "
|
||||
"first; many small turns → batch questions).\n"
|
||||
f"3. **{h3}** — 2-3 app-level actions (compact history, "
|
||||
"smaller model for simple tasks, reuse task outputs instead of re-asking).\n"
|
||||
"Keep the whole reply under 250 words."
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
# ADR-001: 4-Tier Clean Architecture for Desktop Local Application
|
||||
|
||||
* **Status**: ACCEPTED / ENFORCED
|
||||
* **Date**: 2026-08-21
|
||||
* **Deciders**: Team Duy (Tech Lead & AI Runtime), Team Nam (Governance & Automation), Team Hoa (Workspace & Scheduling)
|
||||
* **Target Project**: Cowork Local (Cowork-Local BamBOO)
|
||||
|
||||
---
|
||||
|
||||
## 1. Context and Problem Statement
|
||||
|
||||
Cowork Local is a desktop application written in Python using PySide6 (Qt) and designed for local-first execution.
|
||||
Historically, the codebase suffered from architectural coupling across layers:
|
||||
1. **God-Widget Problem**: Monolithic UI widgets (e.g., `ui/chat_panel.py` >1,800 LOC, `ui/co4e_tab.py` >1,400 LOC) mixed UI rendering, network I/O, business rules, filesystem operations, and background worker lifecycle.
|
||||
2. **Untestable Business Logic**: Core algorithms (model routing, conversation turn management, schedule calculation) were tightly coupled to `PySide6` widgets or `QTimer`, making unit testing in headless CI environments impossible without a graphical display server.
|
||||
3. **Circular Dependencies & Global State Leaks**: Uncontrolled module imports (`model_pricing.py` ↔ `usage_tracker.py`, `agent_security.py` ↔ `agent_security_alert.py`) and mutable global state (`state.py::AppContext.active_project_id`) caused race conditions in background task runs.
|
||||
|
||||
---
|
||||
|
||||
## 2. Decision: 4-Tier Clean Architecture
|
||||
|
||||
We enforce a strict **4-Tier Clean Architecture** based on the Dependency Inversion Principle:
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ PRESENTATION │
|
||||
│ (PySide6 Widgets, Dialogs, Qt Signals/Slots, View Models) │
|
||||
└──────────────────────────────┬──────────────────────────────┘
|
||||
│ depends on
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ APPLICATION │
|
||||
│ (Use Case Services, Turn Orchestrators, Route Dispatchers) │
|
||||
│ *** STRICTLY PURE PYTHON (0 Qt) *** │
|
||||
└──────────────────────────────┬──────────────────────────────┘
|
||||
│ depends on
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ DOMAIN & RUNTIME CORE │
|
||||
│ (Entities, Value Objects, Domain Events, Tool Descriptors) │
|
||||
│ *** STRICTLY PURE PYTHON (0 Qt) *** │
|
||||
└──────────────────────────────▲──────────────────────────────┘
|
||||
│ implemented by
|
||||
┌──────────────────────────────┴──────────────────────────────┐
|
||||
│ INFRASTRUCTURE │
|
||||
│ (LLM Providers, Keyring Secrets, Atomic Persistence, MCP) │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Layer Definitions and Responsibilities
|
||||
|
||||
### Tier 1: Presentation Layer (`presentation/`)
|
||||
* **Responsibilities**: UI component layout, user event capture, progress display, visual animations, confirmation dialog triggers.
|
||||
* **Allowed Imports**: `PySide6.*`, `application.*`, `domain.*`.
|
||||
* **Forbidden**: Direct database queries, raw LLM API calls, disk writes outside UI cache, executing tool commands directly.
|
||||
* **Constraints**: Every widget file must strictly be **under 400 lines of code (LOC)**.
|
||||
|
||||
### Tier 2: Application Layer (`application/`)
|
||||
* **Responsibilities**: Orchestrate single use cases (e.g. `ConversationApplicationService`, `RoutingApplicationService`, `TaskApplicationService`). Convert UI requests into domain requests, coordinate domain services with infrastructure adapters.
|
||||
* **Allowed Imports**: `domain.*`, `infrastructure.*` interfaces/contracts, standard Python libraries.
|
||||
* **Forbidden**: `PySide6`, `PyQt5`, `PyQt6`, `ui.*`, `app.*`.
|
||||
* **Nature**: **100% Pure Python**. Must be executable and testable in headless CI environments without a display driver.
|
||||
|
||||
### Tier 3: Domain Layer (`domain/`)
|
||||
* **Responsibilities**: Core domain models, frozen DTO snapshots (`ConversationExecutionRequest`), typed event streams (`AgentEvent`), descriptors (`ToolDescriptor`, `ProviderDescriptor`), deterministic calculation algorithms (`ScheduleCalculator`).
|
||||
* **Allowed Imports**: Standard Python library only (`dataclasses`, `typing`, `enum`, `datetime`, `pathlib`, `abc`).
|
||||
* **Forbidden**: `PySide6`, `PyQt*`, `requests`, `sqlalchemy`, filesystem mutations, OS network calls.
|
||||
* **Nature**: Completely isolated and zero-dependency core.
|
||||
|
||||
### Tier 4: Infrastructure Layer (`infrastructure/`)
|
||||
* **Responsibilities**: Adapters for external systems (OpenAI/Anthropic/Ollama/FPT providers, OS Keyring via `SecretStore`, `AtomicJsonFile` persistence, MCP child processes, filesystem tools).
|
||||
* **Allowed Imports**: Third-party SDKs, OS libraries, `domain.*`.
|
||||
* **Forbidden**: `presentation.*`, `PySide6.QtWidgets`.
|
||||
|
||||
---
|
||||
|
||||
## 4. Architectural Rules and Non-Negotiable Invariants
|
||||
|
||||
1. **Zero Qt in Business Logic**:
|
||||
- `domain/` and `application/` must never import `PySide6` or `PyQt*`.
|
||||
- Verified via AST parser script `scripts/check_imports.py`.
|
||||
2. **Immutable Request Snapshots**:
|
||||
- Turns are initiated using immutable frozen dataclasses (`ConversationExecutionRequest`) to decouple runtime state from mutable UI state.
|
||||
3. **Thread Safety and Signal Decoupling**:
|
||||
- AI generation and tool calls run asynchronously in worker threads.
|
||||
- UI updates occur strictly on the Qt main thread by consuming `AgentEvent` streams through Qt Signal bridges.
|
||||
4. **Single Responsibility and Modularity**:
|
||||
- Production files must stay within **400 LOC**.
|
||||
5. **English In-Code Comments**:
|
||||
- Every modified or created line/block must include concise English comments explaining design decisions and processing logic.
|
||||
|
||||
---
|
||||
|
||||
## 5. Consequences and Compliance
|
||||
|
||||
* **Positive**:
|
||||
- Full testability: Unit tests run in milliseconds without GUI or network mocks.
|
||||
- Zero circular dependencies: Clear top-down data flow.
|
||||
- Resilience: UI crashes do not corrupt background tasks or files.
|
||||
* **Verification**:
|
||||
- Automated CI gate: `python scripts/check_imports.py` and `python scripts/check_loc.py`.
|
||||
@@ -0,0 +1,101 @@
|
||||
# Báo cáo hoàn tất — extract:co4e_canvas_widget (lane N3 — Co4E Studio)
|
||||
|
||||
- **Ngày:** 2026-08-25
|
||||
- **Người:** hiephv3@fpt.com (N3)
|
||||
- **Nhánh:** `gamma/refactor`
|
||||
- **Lệnh đo dùng xuyên suốt:** `.venv/Scripts/python.exe -m pytest tests -q --tb=no -rA --continue-on-collection-errors`
|
||||
|
||||
---
|
||||
|
||||
## 1. Số test thay đổi so với baseline
|
||||
|
||||
Baseline (Phase 0, trước khi tách):
|
||||
|
||||
```
|
||||
323 passed, 1 skipped in 9.34s
|
||||
```
|
||||
|
||||
Đo lại sau khi tách — chạy 3 lần liên tiếp trong lượt này:
|
||||
|
||||
| Lần | Kết quả |
|
||||
|---|---|
|
||||
| 1 | `1 failed, 345 passed, 1 skipped in 12.25s` — `FAILED tests/test_no_ignored_source.py::test_khong_file_py_nao_bi_bo_quen_chua_theo_doi` |
|
||||
| 2 | `346 passed, 1 skipped in 13.31s` |
|
||||
| 3 | `346 passed, 1 skipped in 12.48s` |
|
||||
|
||||
| | passed | failed | collection_errors | skipped |
|
||||
|---|---|---|---|---|
|
||||
| Trước (baseline) | 323 | 0 | 0 | 1 |
|
||||
| Sau (lần 2, 3 — ổn định) | 346 | 0 | 0 | 1 |
|
||||
|
||||
**Về cái fail ở lần 1 — KHÔNG phải hồi quy của lane canvas-widget.** Tại đúng thời điểm chạy lần 1, `git status --porcelain` cho thấy `tests/characterization/test_co4e_runs_page.py` và `presentation/co4e/co4e_run_control_widget.py` đang ở trạng thái untracked (`??`) — đây là sản phẩm của một phiên Claude khác (lane run-control) đang làm việc song song trên cùng thư mục và chưa kịp `git add`. `test_no_ignored_source` kiểm tra "mọi file `.py` phải đã được git add trong clone sạch", nên nó bắt trúng khoảnh khắc file kia chưa staged — đây chính là kiểu hiện tượng "nhiều phiên Claude song song" mà tài liệu hướng dẫn có cảnh báo. Kiểm tra lại `git status` ngay sau đó xác nhận hai file này đã chuyển sang trạng thái `A` (đã add), và lần đo 2, 3 chạy lại đều xanh ổn định (346 passed, 0 failed cả hai lần) — không phải flaky do code của lane này, không phải do `test_atomic_json` (test flaky đã biết) xuất hiện lần nào trong 3 lần chạy.
|
||||
|
||||
Kết luận: `passed` tăng 323 → 346 (+23, đến từ bộ test đặc tả mới `test_co4e_canvas_widget.py` của lane này cộng với các lane khác đang chạy song song trên cùng nhánh). Không có test fail mới thuộc phạm vi lane canvas-widget. `collection_errors` = 0 ở cả hai mốc. `skipped` giữ nguyên 1 (mốc cũ có giải thích trong docstring, không phải nợ phát sinh từ lane này).
|
||||
|
||||
---
|
||||
|
||||
## 2. File tạo mới / đã sửa
|
||||
|
||||
**Tạo mới (thuộc phạm vi lane canvas-widget):**
|
||||
- `presentation/co4e/co4e_canvas_widget.py`
|
||||
- `presentation/co4e/canvas_items.py`
|
||||
- `presentation/co4e/canvas_interaction_mixin.py`
|
||||
- `tests/characterization/test_co4e_canvas_widget.py`
|
||||
- `docs/architecture/co4e-split-map-canvas-widget.md`
|
||||
- `docs/architecture/co4e-split-map-canvas-widget.json`
|
||||
- `docs/architecture/co4e-refactor-run-report-canvas-widget.md` (chính file này)
|
||||
|
||||
**Đã sửa:**
|
||||
- `ui/co4e_canvas.py` — giữ lại làm shim tương thích ngược, xem mục 5.
|
||||
|
||||
**Không thuộc lane này** (xuất hiện trong `git status --porcelain` chung của repo, do các lane khác — node-property, run-control, v.v. — đang chạy song song, liệt kê để tránh nhận vơ): `application/workflows/co4e_workflow_service.py`, `domain/workflows/run_record.py`, `presentation/co4e/agent_list_panel.py`, `presentation/co4e/canvas_geometry.py`, `presentation/co4e/co4e_tab.py`, `presentation/co4e/node_property_panel.py`, `presentation/co4e/node_property_actions_mixin.py`, `presentation/co4e/skills_list_panel.py`, `presentation/co4e/step_config_section.py`, `presentation/co4e/co4e_run_control_widget.py`, `ui/co4e_tab.py`, `ui/co4e_config_panel.py`, `tests/characterization/test_co4e_agent_panel.py`, `tests/characterization/test_co4e_run_manager_behavior.py`, `tests/characterization/test_co4e_skills_panel.py`, `tests/characterization/test_node_property_panel.py`, `tests/characterization/test_co4e_runs_page.py`, `tests/fakes/fake_co4e_workflow_service.py`, `tests/test_build_co4e_tab.py`, `tests/test_co4e_workflow_service.py`, `docs/architecture/co4e-split-map.md`, `docs/architecture/co4e-split-map.json`, `docs/architecture/co4e-split-map-node-property.md`, `docs/architecture/co4e-split-map-node-property.json`, `docs/architecture/co4e-split-map-run-control.md`, `docs/architecture/co4e-split-map-run-control.json`, `docs/architecture/co4e-refactor-run-report.md`, `docs/architecture/co4e-refactor-run-report-node-property.md`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Kết quả cổng ranh giới
|
||||
|
||||
**ĐỎ (FAIL)** — theo kết quả Phase 5 đã chạy (`clean: false`). Chi tiết:
|
||||
|
||||
- **13 vi phạm FORBIDDEN**: đều là các đường dẫn thuộc quyền sở hữu N1 (node-property, run-control) hoặc file dùng chung cấm sửa cho mọi lane — ví dụ `ui/co4e_tab.py`, `ui/co4e_config_panel.py`, `presentation/co4e/node_property_panel.py`, `node_property_actions_mixin.py`, `step_config_section.py`, `docs/architecture/co4e-split-map-node-property.*`, `co4e-refactor-run-report-node-property.md`, `co4e-split-map.md/json`, `co4e-refactor-run-report.md`, `co4e-split-map-run-control.*`. **Không cái nào trong số này do lane canvas-widget đụng tới trong lượt làm việc này.**
|
||||
- **14 vi phạm OUT-OF-WHITELIST**: `application/workflows/co4e_workflow_service.py`, `domain/workflows/run_record.py`, `presentation/co4e/agent_list_panel.py`, `presentation/co4e/canvas_geometry.py`, `presentation/co4e/co4e_tab.py`, `presentation/co4e/skills_list_panel.py`, `tests/characterization/test_co4e_agent_panel.py`, `tests/characterization/test_co4e_canvas_geometry.py`, `tests/characterization/test_co4e_run_manager_behavior.py`, `tests/characterization/test_co4e_skills_panel.py`, `tests/characterization/test_node_property_panel.py`, `tests/fakes/fake_co4e_workflow_service.py`, `tests/test_build_co4e_tab.py`, `tests/test_co4e_workflow_service.py`, và mới phát sinh `tests/characterization/test_co4e_runs_page.py`. **Cũng không phải sản phẩm của lane canvas-widget** — đây là artefact của các lane khác đang chạy song song, cùng nằm trong `git status` chung của repo vì cổng chặn quét trạng thái toàn repo chứ không tách theo phiên.
|
||||
|
||||
Đối chiếu với whitelist được cấp cho chính lane này (`tests/characterization/test_co4e_canvas_widget.py`, `presentation/co4e/co4e_canvas_widget.py`, `presentation/co4e/canvas_items.py`, `presentation/co4e/canvas_interaction_mixin.py`, `ui/co4e_canvas.py`, `docs/architecture/co4e-split-map-canvas-widget.md/json`): **không có mục nào trong 7 file này xuất hiện trong danh sách vi phạm** — cổng đỏ hoàn toàn do trạng thái git dùng chung của các lane khác chưa dọn/commit, không phải do vi phạm ranh giới của lane canvas-widget.
|
||||
|
||||
---
|
||||
|
||||
## 4. Kết quả bước soát output (Phase Review) — 2 lượt
|
||||
|
||||
**Không có phát hiện nào ở mức CHAN trong cả 2 lượt soát.** Cả hai đều verdict `DAT` (đạt), `repaired: false` (không cần sửa lại trong lúc soát).
|
||||
|
||||
### Lượt 1 — subject: `tests/characterization/test_co4e_canvas_widget.py` (verdict: DAT)
|
||||
- Mức CHAN: không có.
|
||||
- Mức SUA: không có.
|
||||
- Mức GHI_NHAN (4):
|
||||
1. `test_relayout_on_empty_canvas_is_a_safe_noop` (dòng 723-724): assert dựa trên giá trị hardcode `True` ngay sau khi gọi `relayout()` — chỉ là phép thử "không ném exception", không khoá được giá trị/hành vi cụ thể nào của `relayout()` trên canvas rỗng. Không phải lỗi, nhưng không phải lưới an toàn thật cho trường hợp này.
|
||||
2. Comment tại `test_zoom_in_clamps_at_max_after_7_steps_from_1_0` (dòng 686-696) lệch một đơn vị so với tính toán độc lập (chạm 3.0 ở lần áp dụng thứ 8, không phải thứ 7 như comment nói) — nhưng giá trị assert (mảng 20 phần tử) vẫn đúng 100% với hành vi thật, chỉ comment giải thích bị lệch.
|
||||
3. `ui/co4e_canvas.py` có 2 "equivalent mutant" phát hiện qua mutation-test: xoá điều kiện `src != target_id` trong `_finish_connect` và xoá guard `if source == target: return` trong `_make_edge` đều không làm test đỏ, vì các điểm gọi `_make_edge` đã tự lọc `source != target` từ trước — hai lớp bảo vệ trùng nhau. Không phải lỗi hành vi hiện tại, nhưng là điểm cần lưu ý cho người tách sau: nếu một lớp bảo vệ mất đi trong tương lai mà không có lớp kia, quirk "tự nối vào mình" sẽ mất hiệu lực mà test này không bắt được tại đúng điểm đó.
|
||||
4. Ghi nhận về git status: 2 file `docs/architecture/co4e-split-map-run-control.md/json` xuất hiện thêm giữa lúc soát — xác nhận là sản phẩm của một agent khác chạy song song (cùng mtime), không phải do lượt soát này gây ra; `ui/co4e_canvas.py` không đổi trạng thái và pytest 43/43 vẫn xanh sau khi khôi phục mutation.
|
||||
|
||||
### Lượt 2 — subject: `presentation/co4e/co4e_canvas_widget.py` (verdict: DAT)
|
||||
- Mức CHAN: không có.
|
||||
- Mức SUA (1): `co4e_canvas_widget.py` dòng 75-89 (`Co4ECanvas.load`) — điểm chuyển từ domain (list Node/Edge) sang item hiển thị thiếu comment giải thích quirk "edge có source/target không nằm trong `self._nodes` bị âm thầm bỏ qua". Quirk có thật, kế thừa nguyên văn từ `ui/co4e_canvas.py` gốc (không phải lỗi mới do lần tách này gây ra — đã đối chứng bằng `git show HEAD`), nhưng theo yêu cầu B4 (bắt buộc có comment tại các điểm chuyển tầng DTO), cần bổ sung comment trong lượt sửa tiếp theo.
|
||||
- Mức GHI_NHAN (4):
|
||||
1. Hai khối comment mâu thuẫn nhau trong `canvas_items.py` dòng 128-133 (`_NodeItem.paint`) về vị trí cổng (một khối nói "top-center/bottom-center", khối liền sau nói "left-center/right-center" — code thực tế vẽ left-center/right-center). Lỗi tồn tại sẵn trong bản gốc, được dời nguyên văn đúng kỷ luật B1 "không đổi", không phải lỗi mới. Cần dọn ở lượt sau.
|
||||
2. Docstring của 3 file (`co4e_canvas_widget.py`, `canvas_items.py`, `canvas_interaction_mixin.py`) dẫn số dòng cụ thể của `ui/co4e_canvas.py` (ví dụ "289-317", "701 dòng tổng") không khớp với `ui/co4e_canvas.py` tại git HEAD hiện tại (791 dòng, class `Co4ECanvas` bắt đầu ở dòng 379, không phải 289). Nội dung code đã được xác minh khớp 100% bằng AST diff độc lập — đây là vấn đề chất lượng tài liệu (số dòng tham chiếu một trạng thái trung gian chưa commit), không phải lỗi hành vi.
|
||||
3. Suite tổng đỏ 1 test (`test_no_ignored_source`) tại thời điểm soát, do file `tests/characterization/test_co4e_runs_page.py` (thuộc nhánh tách khác) chưa được `git add` — không liên quan 3 file thuộc phạm vi soát này, đã tự hết khi lane kia add file (khớp với mục 1 của báo cáo này).
|
||||
4. `docs/architecture/co4e-refactor-run-report-canvas-widget.md` chưa được tạo tại thời điểm soát — ghi nhận thiếu deliverable, không chặn. (Báo cáo này chính là file được yêu cầu tạo, viết trong lượt hiện tại.)
|
||||
|
||||
---
|
||||
|
||||
## 5. Việc để lại cho lần sau
|
||||
|
||||
- **`ui/co4e_canvas.py` vẫn còn chạy song song** với `presentation/co4e/co4e_canvas_widget.py` — hiện đóng vai trò shim/re-export để các chỗ import cũ (`from .co4e_canvas import Co4ECanvas, CO4E_MIME`) không vỡ. Cần dọn các nơi còn import theo đường cũ, chuyển sang import trực tiếp từ `presentation/co4e/co4e_canvas_widget.py`, rồi mới an toàn để rút gọn/xoá shim.
|
||||
- **1 điểm SUA từ lượt soát 2 chưa được sửa**: bổ sung comment giải thích quirk bỏ-qua-edge-mồ-côi tại `presentation/co4e/co4e_canvas_widget.py` (hàm `load`, dòng 75-89) theo đúng yêu cầu B4.
|
||||
- **2 GHI_NHAN không chặn nhưng nên dọn cùng đợt sau**: (a) comment mâu thuẫn vị trí cổng trong `canvas_items.py` dòng 128-133 (kế thừa từ bản gốc); (b) số dòng tham chiếu trong docstring của 3 file mới không khớp trạng thái HEAD hiện tại — cần cập nhật lại số dòng khi file được commit để người đọc sau đối chiếu lại được.
|
||||
- `docs/architecture/co4e-split-map-canvas-widget.md/json` là input cho bước dọn shim `ui/co4e_canvas.py` ở lượt tiếp theo.
|
||||
- Cổng ranh giới hiện đang đỏ do trạng thái git dùng chung của nhiều lane chưa commit/dọn — cần các lane liên quan (node-property, run-control, v.v.) tự commit hoặc dọn phần của mình để cổng có thể xanh trở lại cho toàn repo; lane canvas-widget không có vi phạm nào trong whitelist của chính nó (xem mục 3).
|
||||
|
||||
## 6. Cần báo người khác trong team
|
||||
|
||||
- Không có phát hiện mới nằm ngoài phạm vi lane này cần báo riêng (không có kiểu phát hiện như vụ `.gitignore` nuốt `infrastructure/secrets/` trước đây).
|
||||
- Đáng lưu ý (không cần hành động thêm, chỉ để các lane khác biết): trong lúc đo baseline lần 1 của lượt này, `test_no_ignored_source` đỏ thoáng qua vì lane run-control (`tests/characterization/test_co4e_runs_page.py`, `presentation/co4e/co4e_run_control_widget.py`) chưa kịp `git add` hai file mới của họ. Tự hết ở lần đo thứ 2 sau khi họ add xong. Gợi ý: các lane nên `git add` sớm sau khi tạo file mới để tránh gate/test đỏ giả khi nhiều phiên Claude chạy song song trên cùng thư mục.
|
||||
@@ -0,0 +1,156 @@
|
||||
# Báo cáo refactor — Chat view / Composer (widget cuối) — làn N3 Co4E Studio
|
||||
|
||||
- **Ngày:** 2026-08-25
|
||||
- **Người:** Lâm (hiephv3@fpt.com)
|
||||
- **Nhánh:** `gamma/refactor`
|
||||
- **Phạm vi:** trích xuất `_ChatInput`, `_skill_names`, `_agent_names`, `_directive_token`
|
||||
và phần dựng UI của `ChatPanel` (từ `_build_chat` cũ) trong `ui/co4e_tab.py` sang
|
||||
`presentation/co4e/co4e_chat_view.py`.
|
||||
|
||||
---
|
||||
|
||||
## 1. Số test thay đổi thế nào so với baseline
|
||||
|
||||
Lệnh đo (chạy lại đúng lệnh baseline ngay trước khi viết báo cáo này):
|
||||
|
||||
```
|
||||
.venv/Scripts/python.exe -m pytest tests -q --tb=no -rA --continue-on-collection-errors
|
||||
```
|
||||
|
||||
Kết quả vừa đo:
|
||||
|
||||
```
|
||||
347 passed, 1 skipped in 8.10s
|
||||
```
|
||||
|
||||
So với baseline được giao (Phase 0): `346 passed, 0 failed, 0 collection_errors, 1 skipped`.
|
||||
|
||||
| | Trước (Phase 0) | Sau (vừa đo lại) |
|
||||
|---|---|---|
|
||||
| passed | 346 | 347 |
|
||||
| failed | 0 | 0 |
|
||||
| collection_errors | 0 | 0 |
|
||||
| skipped | 1 (giữ nguyên, lý do đã biết — thứ tự import `CONFIG_DIR` giữa các file test, không liên quan chat view) | 1 (cùng lý do) |
|
||||
|
||||
**+1 passed** đúng bằng đúng 1 test mới `tests/characterization/test_co4e_chat_view.py`
|
||||
được thêm trong đợt này. Không có test fail mới, không có collection error mới →
|
||||
**không hồi quy**.
|
||||
|
||||
- Lỗi MỐC CŨ có từ trước (nợ của làn khác): **không có** — 0 collection error, 0 failed
|
||||
ở cả trước và sau.
|
||||
- Flaky đã biết (`tests/test_atomic_json.py::test_ghi_de_nhieu_lan_van_dung`,
|
||||
`PermissionError [WinError 5]` khi Windows giữ khoá file tạm): **không xuất hiện**
|
||||
trong lần đo cuối cùng này (347 passed, 0 failed). Trong lượt soát trước đó nó có
|
||||
xuất hiện đúng 1 lần trên 2 lần chạy (`1 failed, 346 passed, 1 skipped`), rồi lần
|
||||
chạy kế tiếp lại xanh (`347 passed, 1 skipped`) — đúng đặc điểm flaky đã biết, không
|
||||
quy cho đợt trích xuất này.
|
||||
|
||||
## 2. File đã tạo, file đã sửa
|
||||
|
||||
Theo `git status --porcelain` (đối chiếu với whitelist được cấp cho lượt chat-view):
|
||||
|
||||
**Tạo mới (thuộc lượt này):**
|
||||
- `presentation/co4e/co4e_chat_view.py` — file production mới (258 dòng, ≤ 400 dòng,
|
||||
chỉ import PySide6.QtCore/QtWidgets, không đụng domain/application).
|
||||
- `tests/characterization/test_co4e_chat_view.py` — test đặc trưng hoá hành vi cũ.
|
||||
- `docs/architecture/co4e-split-map-chat-view.md`
|
||||
- `docs/architecture/co4e-split-map-chat-view.json`
|
||||
- `docs/architecture/co4e-refactor-run-report-chat-view.md` — chính file báo cáo này
|
||||
(trước lượt này file chưa tồn tại — đây là khoảng thiếu mà bước soát đã ghi nhận,
|
||||
nay bù lại).
|
||||
|
||||
**Sửa (thuộc lượt này):**
|
||||
- `ui/co4e_tab.py` — dây lại để dùng `ChatPanel`/`_ChatInput` từ module mới thay vì
|
||||
định nghĩa tại chỗ.
|
||||
|
||||
**Các mục khác trong `git status` (canvas widget, node property, run control, agent
|
||||
panel, run manager, v.v.) không thuộc lượt chat-view** — đó là dấu vết của các làn/
|
||||
phiên khác đang chạy song song trên cùng thư mục làm việc (repo này có nhiều phiên
|
||||
Claude chạy đồng thời). Không đụng, không sửa trong lượt này.
|
||||
|
||||
## 3. Cổng chặn (Phase 5)
|
||||
|
||||
**Kết quả: ĐỎ** (`"clean": false`).
|
||||
|
||||
Tuy nhiên toàn bộ vi phạm liệt kê **không thuộc phần lượt chat-view đã viết ra** — kiểm
|
||||
tra riêng 5 đường dẫn thuộc whitelist của lượt này
|
||||
(`presentation/co4e/co4e_chat_view.py`, `tests/characterization/test_co4e_chat_view.py`,
|
||||
`docs/architecture/co4e-split-map-chat-view.md`, `.json`, `ui/co4e_tab.py`): **không có
|
||||
đường dẫn nào trong 5 file này xuất hiện trong danh sách `violations`.**
|
||||
|
||||
Danh sách vi phạm thật (đỏ) đến từ file của các làn khác đang tồn tại chung trong working
|
||||
tree (không do lượt chat-view tạo ra):
|
||||
|
||||
- `FORBIDDEN` (khớp `forbidden_paths` của N1): `presentation/co4e/canvas_interaction_mixin.py`,
|
||||
`canvas_items.py`, `co4e_canvas_widget.py`, `co4e_run_control_widget.py`,
|
||||
`node_property_actions_mixin.py`, `node_property_panel.py`, `step_config_section.py`,
|
||||
`ui/co4e_canvas.py`, `ui/co4e_config_panel.py`, cùng các `docs/architecture/co4e-split-map*.md/json`
|
||||
và `co4e-refactor-run-report*.md` khác của N1.
|
||||
- `OUTSIDE_WHITELIST` (file mới không khớp `allowed_write_globs` của *lượt này*):
|
||||
`application/workflows/co4e_workflow_service.py`, `domain/workflows/run_record.py`,
|
||||
`presentation/co4e/agent_list_panel.py`, `canvas_geometry.py`,
|
||||
`presentation/co4e/co4e_tab.py` (khác `ui/co4e_tab.py` được phép), `skills_list_panel.py`,
|
||||
và các test/fakes tương ứng (`test_co4e_agent_panel.py`, `test_co4e_canvas_geometry.py`,
|
||||
`test_co4e_canvas_widget.py`, `test_co4e_run_manager_behavior.py`, `test_co4e_runs_page.py`,
|
||||
`test_co4e_skills_panel.py`, `test_node_property_panel.py`, `fake_co4e_workflow_service.py`,
|
||||
`test_build_co4e_tab.py`, `test_co4e_workflow_service.py`).
|
||||
|
||||
Kết luận: cổng chặn báo đỏ ở mức **toàn working tree**, không phải đỏ do lượt
|
||||
chat-view — vì cổng chặn quét nguyên `git status` chung, còn nhiều làn/phiên khác đang
|
||||
ghi đè cùng thư mục. Phần việc riêng của lượt chat-view (5 đường dẫn whitelist) **sạch**.
|
||||
|
||||
## 4. Bước soát output — 2 lượt
|
||||
|
||||
Không có phát hiện ở mức **CHAN** (chặn) trong bất kỳ lượt soát nào — cả hai lượt đều
|
||||
có `verdict: "DAT"` (đạt).
|
||||
|
||||
**Lượt 1 — soát `tests/characterization/test_co4e_chat_view.py`:** verdict **DAT**.
|
||||
Một phát hiện mức `SUA`:
|
||||
- Dòng 193: `assert ci._popup.width() == max(280, ci.width())` — assertion rỗng nghĩa
|
||||
cho riêng claim "280" vì `ci.width()` mặc định (chưa show/setFixedWidth) là 640 trong
|
||||
môi trường test, luôn thắng trong `max()` bất kể 280 đổi thành gì < 640. Xác nhận bằng
|
||||
mutation thật (280→300 trong `ui/co4e_tab.py`): test vẫn xanh (`1 passed in 2.01s`).
|
||||
23 case còn lại trong file đều bắt được mutation tương ứng (11/12 hành vi bị làm hỏng
|
||||
→ test đỏ đúng như kỳ vọng).
|
||||
|
||||
**Lượt 2 — soát `presentation/co4e/co4e_chat_view.py`:** verdict **DAT**. Hai phát hiện
|
||||
mức `SUA`, một mức `GHI_NHAN`:
|
||||
- `SUA`: `docs/architecture/co4e-refactor-run-report-chat-view.md` (đúng file này) chưa
|
||||
tồn tại tại thời điểm soát — thiếu deliverable bắt buộc của quy trình. **Đã bù lại
|
||||
bằng chính báo cáo này.**
|
||||
- `SUA`: docstring module (dòng 6-7, 15-18) trích sai số dòng gốc trong `ui/co4e_tab.py`
|
||||
(lệch 3-35 dòng, ví dụ `_build_chat` ghi "1030-1093" nhưng thật là "1065-1128" theo
|
||||
`git show HEAD`). Không ảnh hưởng hành vi — thân hàm đã được đối chiếu bằng
|
||||
`ast.get_source_segment` + `difflib` và **IDENTICAL** 100% với bản gốc. Đây là lỗi
|
||||
trích dẫn tài liệu, chưa sửa trong lượt viết báo cáo này (ngoài phạm vi được giao cho
|
||||
lượt này — chỉ viết báo cáo, không sửa code sản xuất).
|
||||
- `GHI_NHAN`: hai file `docs/architecture/co4e-refactor-run-report-node-property.md` và
|
||||
`co4e-refactor-run-report-run-control.md` (thuộc `forbidden_paths` của lượt chat-view)
|
||||
có thay đổi/tồn tại — nhưng nội dung xác nhận thuộc lane khác (node-property, run-control),
|
||||
không phải do lượt chat-view đụng vào. Không quy lỗi cho lượt này.
|
||||
|
||||
Ngoài ra, bước soát đã tự chạy lại bộ test 2 lần độc lập để loại trừ flaky trước khi kết
|
||||
luận: lần 1 gặp `1 failed` (đúng flaky `test_atomic_json` đã biết), lần 2 `347 passed,
|
||||
1 skipped, 0 failed` — nhất quán với con số ở mục 1.
|
||||
|
||||
## 5. Việc để lại cho lần chạy sau / cần báo người khác
|
||||
|
||||
**Để lại cho lần sau (trong phạm vi lượt chat-view, chưa làm ở lượt viết báo cáo này):**
|
||||
- Sửa docstring module trong `presentation/co4e/co4e_chat_view.py` (dòng 6-7, 15-18) để
|
||||
khớp đúng số dòng thật trong `ui/co4e_tab.py` (HEAD): `_skill_names` 60-64,
|
||||
`_agent_names` 67-70, `_directive_token` 121-133, `_ChatInput` 136-225, `_build_chat`
|
||||
1065-1128 — hiện ghi sai (63-73, 124-228, 1030-1093).
|
||||
- Làm chặt lại assertion popup-width-floor tại `tests/characterization/test_co4e_chat_view.py:193`
|
||||
(claim "280" hiện không được khoá thật vì `ci.width()` mặc định 640 luôn thắng trong
|
||||
`max()`) — cần set `ci` về chiều rộng nhỏ hơn 280 trước khi assert, hoặc mock riêng, để
|
||||
test thực sự khoá hằng số 280.
|
||||
- `ui/co4e_tab.py` vẫn còn nhiều phần khác chưa tách (không thuộc phạm vi widget
|
||||
chat/composer) — các widget khác đã có báo cáo riêng của N1
|
||||
(`canvas-widget`, `node-property`, `run-control`).
|
||||
|
||||
**Cần báo người khác trong team:** không có phát hiện mới nào ngoài phạm vi làn này cần
|
||||
escalate ở lượt này. Ghi nhận (không phải lỗi mới, chỉ là quan sát): repo đang có nhiều
|
||||
phiên Claude/nhiều làn chạy song song trên cùng một working tree, khiến cổng chặn của
|
||||
lượt chat-view báo đỏ ở mức toàn cục do file của các làn khác — điều này không phải do
|
||||
lượt chat-view gây ra và không cần hành động thêm từ N3, nhưng đội điều phối nên biết để
|
||||
không hiểu nhầm là lượt này làm vỡ ranh giới của N1.
|
||||
@@ -0,0 +1,226 @@
|
||||
# Báo cáo chạy — tách `StepConfigPanel` khỏi `ui/co4e_config_panel.py`
|
||||
|
||||
- **Ngày:** 2026-08-24
|
||||
- **Người:** hiephv3@fpt.com
|
||||
- **Nhánh:** gamma/refactor
|
||||
- **Làn:** N3 — Co4E Studio, phạm vi `node_property_panel`
|
||||
|
||||
Ghi chú: repo này có nhiều phiên Claude chạy song song trên cùng một thư mục
|
||||
làm việc. Báo cáo dưới đây chỉ trả lời 5 câu bắt buộc, không lan sang phần đã
|
||||
viết ở các lượt trước (chi tiết cắt-dán từng dòng xem
|
||||
`docs/architecture/co4e-split-map-node-property.md`/`.json`).
|
||||
|
||||
## 1. Số test thay đổi thế nào so với baseline
|
||||
|
||||
Lệnh đo (baseline Phase 0, chạy trước khi làn này bắt đầu):
|
||||
|
||||
```
|
||||
.venv/Scripts/python.exe -m pytest tests -q --tb=no -rA --continue-on-collection-errors
|
||||
279 passed, 1 skipped
|
||||
```
|
||||
|
||||
Lệnh đo lại (chạy ngay bây giờ, sau khi làn này đã xong):
|
||||
|
||||
```
|
||||
$ .venv/Scripts/python.exe -m pytest tests -q --tb=no -rA --continue-on-collection-errors
|
||||
330 passed, 1 skipped in 10.15s
|
||||
```
|
||||
|
||||
| | passed | failed | collection_errors | skipped |
|
||||
|---|---|---|---|---|
|
||||
| Trước (Phase 0) | 279 | 0 | 0 | 1 |
|
||||
| Sau (bây giờ) | 330 | 0 | 0 | 1 |
|
||||
|
||||
- **Không có test fail nào**, mới hay cũ. `passed` sau ≥ passed trước
|
||||
(330 ≥ 279) → không hồi quy theo tiêu chí so lệch.
|
||||
- Chênh lệch +51 **không** đến từ riêng làn này. Làn này chỉ thêm đúng 1 test
|
||||
mới (`tests/characterization/test_node_property_panel.py`). +50 còn lại đến
|
||||
từ các làn song song khác đã nhập vào cùng thư mục làm việc trong lúc làn
|
||||
này chạy (`test_co4e_agent_panel.py`, `test_co4e_canvas_geometry.py`,
|
||||
`test_co4e_canvas_widget.py`, `test_co4e_skills_panel.py`,
|
||||
`test_build_co4e_tab.py`, `test_co4e_workflow_service.py`, ...) — thấy rõ
|
||||
trong `git status --porcelain` ở câu 2, không phải việc của làn N3.
|
||||
- **1 skip** là mốc cũ có từ trước, không phải do lượt này gây ra:
|
||||
`tests/characterization/test_co4e_run_manager_behavior.py:140` — tự skip
|
||||
vì `cowork_local.config` bị file test khác import với `HOME` thật trước nó
|
||||
trong cùng phiên pytest (giới hạn đã biết, ghi rõ trong docstring đầu file
|
||||
đó, không liên quan `StepConfigPanel`).
|
||||
- Test flaky đã biết của repo
|
||||
(`tests/test_atomic_json.py::test_ghi_de_nhieu_lan_van_dung`, do
|
||||
`os.replace()` gặp khoá file tạm trên Windows) **không xuất hiện** trong
|
||||
lần chạy này — không có gì để báo thêm về nó.
|
||||
- Chưa cần chạy lại lần hai để xác nhận vì không có test nào đỏ ở cả hai lần
|
||||
đo (Phase 0 và bây giờ) — không có ca nghi flaky cần phân xử.
|
||||
|
||||
## 2. File đã tạo, file đã sửa
|
||||
|
||||
`git status --porcelain` (nguyên văn, chạy ngay lúc viết báo cáo):
|
||||
|
||||
```
|
||||
AM application/workflows/co4e_workflow_service.py
|
||||
A docs/architecture/co4e-refactor-run-report-node-property.md
|
||||
A docs/architecture/co4e-split-map-node-property.json
|
||||
A docs/architecture/co4e-split-map-node-property.md
|
||||
A domain/workflows/run_record.py
|
||||
A presentation/co4e/agent_list_panel.py
|
||||
A presentation/co4e/canvas_geometry.py
|
||||
A presentation/co4e/co4e_tab.py
|
||||
A presentation/co4e/node_property_actions_mixin.py
|
||||
A presentation/co4e/node_property_panel.py
|
||||
A presentation/co4e/skills_list_panel.py
|
||||
A presentation/co4e/step_config_section.py
|
||||
A tests/characterization/test_co4e_agent_panel.py
|
||||
AM tests/characterization/test_co4e_canvas_geometry.py
|
||||
A tests/characterization/test_co4e_canvas_widget.py
|
||||
AM tests/characterization/test_co4e_run_manager_behavior.py
|
||||
AM tests/characterization/test_co4e_skills_panel.py
|
||||
A tests/characterization/test_node_property_panel.py
|
||||
A tests/fakes/fake_co4e_workflow_service.py
|
||||
A tests/test_build_co4e_tab.py
|
||||
AM tests/test_co4e_workflow_service.py
|
||||
M ui/co4e_canvas.py
|
||||
M ui/co4e_config_panel.py
|
||||
M ui/co4e_tab.py
|
||||
?? .codegraph/
|
||||
?? cowork_local
|
||||
?? docs/architecture/co4e-refactor-run-report.md
|
||||
?? docs/architecture/co4e-split-map.json
|
||||
?? docs/architecture/co4e-split-map.md
|
||||
?? run_app.bat
|
||||
?? stop_running.ps1
|
||||
```
|
||||
|
||||
**Của đúng làn N3 (`node_property_panel`) — khớp danh sách đường dẫn được
|
||||
cấp quyền:**
|
||||
|
||||
- Mới: `presentation/co4e/node_property_panel.py` (293 dòng),
|
||||
`presentation/co4e/node_property_actions_mixin.py` (202 dòng),
|
||||
`presentation/co4e/step_config_section.py` (134 dòng),
|
||||
`tests/characterization/test_node_property_panel.py`,
|
||||
`docs/architecture/co4e-split-map-node-property.md`,
|
||||
`docs/architecture/co4e-split-map-node-property.json`,
|
||||
`docs/architecture/co4e-refactor-run-report-node-property.md` (chính file
|
||||
này).
|
||||
- Sửa: `ui/co4e_config_panel.py` (528 dòng → 14 dòng, chỉ còn re-export
|
||||
`StepConfigPanel`).
|
||||
- Cả 3 file production mới đều ≤ 400 dòng (293/202/134).
|
||||
|
||||
**Không thuộc làn N3 — xuất hiện trong `git status` vì có phiên khác đang
|
||||
chạy song song trên cùng thư mục, làn này không đụng tới:**
|
||||
`application/workflows/co4e_workflow_service.py`,
|
||||
`domain/workflows/run_record.py`, `presentation/co4e/agent_list_panel.py`,
|
||||
`presentation/co4e/canvas_geometry.py`, `presentation/co4e/co4e_tab.py`,
|
||||
`presentation/co4e/skills_list_panel.py`,
|
||||
`tests/characterization/test_co4e_agent_panel.py`,
|
||||
`tests/characterization/test_co4e_canvas_geometry.py`,
|
||||
`tests/characterization/test_co4e_canvas_widget.py`,
|
||||
`tests/characterization/test_co4e_run_manager_behavior.py`,
|
||||
`tests/characterization/test_co4e_skills_panel.py`,
|
||||
`tests/fakes/fake_co4e_workflow_service.py`, `tests/test_build_co4e_tab.py`,
|
||||
`tests/test_co4e_workflow_service.py`, `ui/co4e_canvas.py`,
|
||||
`ui/co4e_tab.py`, cùng các file untracked
|
||||
`docs/architecture/co4e-split-map.md`/`.json`,
|
||||
`docs/architecture/co4e-refactor-run-report.md`, `.codegraph/`,
|
||||
`cowork_local`, `run_app.bat`, `stop_running.ps1`.
|
||||
|
||||
## 3. Cổng ranh giới: xanh hay đỏ
|
||||
|
||||
**Đỏ** (`clean: false`), nhưng **không phải vì làn N3 tự ý sửa các file cấm**
|
||||
— tất cả vi phạm đều là file thuộc phạm vi làn khác (N1) hoặc làn song song
|
||||
khác đang có mặt trong cùng thư mục làm việc tại thời điểm chạy cổng chặn:
|
||||
|
||||
- **FORBIDDEN** (đúng danh sách cấm tuyệt đối của N3, thuộc làn N1):
|
||||
`ui/co4e_tab.py` (M), `ui/co4e_canvas.py` (M),
|
||||
`docs/architecture/co4e-split-map.md` (mới), `docs/architecture/co4e-split-map.json` (mới),
|
||||
`docs/architecture/co4e-refactor-run-report.md` (mới).
|
||||
- **OUTSIDE-WHITELIST** (không khớp `allowed_write_globs` của N3):
|
||||
`application/workflows/co4e_workflow_service.py`,
|
||||
`domain/workflows/run_record.py`,
|
||||
`presentation/co4e/agent_list_panel.py`,
|
||||
`presentation/co4e/canvas_geometry.py`, `presentation/co4e/co4e_tab.py`,
|
||||
`presentation/co4e/skills_list_panel.py`,
|
||||
`tests/characterization/test_co4e_agent_panel.py`,
|
||||
`tests/characterization/test_co4e_canvas_geometry.py`,
|
||||
`tests/characterization/test_co4e_canvas_widget.py`,
|
||||
`tests/characterization/test_co4e_run_manager_behavior.py`,
|
||||
`tests/characterization/test_co4e_skills_panel.py`,
|
||||
`tests/fakes/fake_co4e_workflow_service.py`, `tests/test_build_co4e_tab.py`,
|
||||
`tests/test_co4e_workflow_service.py`.
|
||||
- `loc_over_cap`: rỗng — không file nào của làn N3 vượt 400 dòng.
|
||||
- `pyside_leaks`: rỗng — không có import PySide6/PyQt trong
|
||||
`domain/`/`application/` (kiểm bằng AST parse, không phải grep).
|
||||
|
||||
Kết luận: 8 file đúng phạm vi được cấp cho làn N3 (danh sách 400-file ở đầu
|
||||
prompt) đều nằm gọn trong whitelist, không file nào của làn N3 chạm vào danh
|
||||
sách cấm. Cổng đỏ là do **trạng thái chung của working tree** (nhiều làn ghi
|
||||
song song), không phải hồi quy do thay đổi của làn này gây ra. Người quyết
|
||||
định commit cần biết: nếu commit y nguyên `git status` hiện tại, sẽ commit
|
||||
luôn cả các thay đổi của những làn khác (N1 và các làn Co4E khác) — cần
|
||||
tách bằng `git add` đúng danh sách 8 file của N3 trước khi commit, không
|
||||
`git add -A`.
|
||||
|
||||
## 4. Bước soát output — 2 lượt, tách riêng theo mức `CHAN`
|
||||
|
||||
**Không lượt soát nào phát hiện mức `CHAN`.** (Mức `CHAN` = test không bắt
|
||||
được lỗi, hoặc code bị viết lại thay vì dời — không có trường hợp nào như
|
||||
vậy trong cả 2 lượt.)
|
||||
|
||||
- **Lượt 1 — chủ thể `tests/characterization/test_node_property_panel.py`,
|
||||
verdict: ĐẠT.**
|
||||
1 phát hiện mức **SUA** (không phải `CHAN`): file test mới chưa
|
||||
`git add`, nên khi chạy full suite làm đỏ
|
||||
`tests/test_no_ignored_source.py::test_khong_file_py_nao_bi_bo_quen_chua_theo_doi`.
|
||||
Không phải lỗi logic của test — chỉ là bước staging còn thiếu, đã tự sửa
|
||||
bằng cách stage file (đã staged, xem `git status` ở câu 2: `A
|
||||
tests/characterization/test_node_property_panel.py`).
|
||||
Xác nhận qua bite-test: phá 14/14 hành vi riêng của
|
||||
`ui/co4e_config_panel.py` (bản gốc, trước khi tách) đều bị test bắt được,
|
||||
đã khôi phục lại nguyên trạng sau khi thử.
|
||||
|
||||
- **Lượt 2 — chủ thể `presentation/co4e/node_property_panel.py`, verdict:
|
||||
ĐẠT.**
|
||||
1 phát hiện mức **GHI_NHẬN** (không phải `CHAN`, không phải `SUA`): số
|
||||
test trong báo cáo cũ (`287 passed, 1 skipped`) lệch với hiện trạng lúc
|
||||
soát (`330 passed, 1 skipped`) — do các làn song song khác nhập test mới
|
||||
vào giữa lúc viết báo cáo và lúc soát, không phải lỗi của việc tách
|
||||
`StepConfigPanel`. Báo cáo này (bản viết lại) đã cập nhật đúng số thật
|
||||
330/1 ở mục 1.
|
||||
Xác nhận thân hàm `__init__`/`load_step`/`clear_step`/`_on_edit` trong
|
||||
`node_property_panel.py` **giống byte-for-byte** (240/240 dòng) với bản
|
||||
gốc `ui/co4e_config_panel.py` tại HEAD; 8 method trong
|
||||
`node_property_actions_mixin.py` chỉ khác đúng độ sâu import
|
||||
(`..core` → `...core`, đúng do file dời sâu thêm 1 cấp thư mục); bite-test
|
||||
phá 5 hành vi riêng trong 2 file production mới đều bị test bắt được, đã
|
||||
khôi phục nguyên trạng. `tools/check_co4e.py` (cổng kiểm soát riêng): giữ
|
||||
nguyên `27/27` control cũ.
|
||||
|
||||
## 5. Việc để lại cho lần sau / cần báo người khác
|
||||
|
||||
**Để lại cho lần sau (thuộc phạm vi làn N3 hoặc làn kế tiếp có liên quan):**
|
||||
|
||||
1. `docs/architecture/co4e-split-map.md` (làn N1, không được sửa ở đây) vẫn
|
||||
liệt kê `node_property_panel.py` như một đích còn dở của việc tách
|
||||
`ui/co4e_tab.py`/`ui/co4e_canvas.py` qua Signal
|
||||
`node_selected`/`node_activated`. Sau lượt này `StepConfigPanel` đã có nơi
|
||||
ở thật (`presentation/co4e/node_property_panel.py`); làn phụ trách tách
|
||||
`ui/co4e_tab.py` có thể import thẳng từ đó thay vì qua
|
||||
`ui/co4e_config_panel.py`, nhưng đó là quyết định của làn N1, không tự
|
||||
đổi ở đây.
|
||||
2. `_ai_draft`/`_load_models` trong `node_property_actions_mixin.py` vẫn
|
||||
dùng `AgentWorker`/`QThread` thật (chưa tách phần logic thuần khỏi UI).
|
||||
Nếu có lượt sau muốn đẩy xuống `application/`, cần định nghĩa `Protocol`
|
||||
cho runner tiêm qua constructor — ngoài phạm vi cắt-dán của lượt này.
|
||||
3. `ui/co4e_config_panel.py` (14 dòng) vẫn còn sống song song làm lớp
|
||||
re-export — chưa xoá, vì `ui/co4e_tab.py` (thuộc N1) còn import từ đó.
|
||||
Xoá file này là quyết định của người sở hữu `ui/co4e_tab.py`.
|
||||
|
||||
**Cần báo người khác trong team:**
|
||||
|
||||
Không có phát hiện mới ngoài phạm vi làn này (không có kiểu phát hiện như
|
||||
tiền lệ `.gitignore`/`secrets/` nêu trong hướng dẫn). Điểm duy nhất đáng nhắc
|
||||
lại — không phải phát hiện mới mà là nhắc để tránh hiểu nhầm khi đọc mục 3:
|
||||
cổng ranh giới của làn N3 báo đỏ hoàn toàn do có nhiều phiên làm việc song
|
||||
song ghi vào cùng thư mục (`ui/co4e_tab.py`, `ui/co4e_canvas.py`,
|
||||
`presentation/co4e/agent_list_panel.py`, v.v. — thuộc N1 và các làn Co4E
|
||||
khác), không phải do thay đổi của làn N3. Ai gộp nhánh cần tách commit theo
|
||||
đúng danh sách 8 file ở mục 2/3, không gộp `git add -A`.
|
||||
@@ -0,0 +1,149 @@
|
||||
# Báo cáo refactor — Flow Status (Runs page) — làn N3 Co4E Studio
|
||||
|
||||
- **Ngày:** 2026-08-25
|
||||
- **Người:** Lâm (N3 — Co4E Studio), hiephv3@fpt.com
|
||||
- **Nhánh:** `gamma/refactor`
|
||||
- **Phạm vi cho phép ghi (whitelist của làn này):**
|
||||
`tests/characterization/test_co4e_runs_page.py`,
|
||||
`presentation/co4e/co4e_run_control_widget.py`,
|
||||
`docs/architecture/co4e-split-map-run-control.md`,
|
||||
`docs/architecture/co4e-split-map-run-control.json`,
|
||||
`ui/co4e_tab.py`,
|
||||
`docs/architecture/co4e-refactor-run-report-run-control.md`
|
||||
|
||||
---
|
||||
|
||||
## 1. Số test thay đổi thế nào so với baseline
|
||||
|
||||
Lệnh đo (chạy lại đúng nguyên văn):
|
||||
|
||||
```
|
||||
.venv/Scripts/python.exe -m pytest tests -q --tb=no -rA --continue-on-collection-errors
|
||||
```
|
||||
|
||||
| Mốc | passed | failed | collection_errors | skipped |
|
||||
|---|---|---|---|---|
|
||||
| Baseline (Phase 0) | 323 | 0 | 0 | 1 |
|
||||
| Sau lượt này (đo lại vừa xong) | 346 | 0 | 0 | 1 |
|
||||
|
||||
Kết quả đo lại, 15 dòng cuối nguyên văn:
|
||||
|
||||
```
|
||||
PASSED tests/test_settings_facade.py::test_provider_doc_duoc_ba_truong
|
||||
PASSED tests/test_settings_facade.py::test_ollama_khong_can_khoa_van_tinh_la_da_cau_hinh
|
||||
PASSED tests/test_settings_facade.py::test_thieu_model_thi_chua_cau_hinh
|
||||
PASSED tests/test_settings_facade.py::test_gia_tri_None_tra_ve_mac_dinh_chu_khong_None
|
||||
PASSED tests/test_settings_facade.py::test_routing_kieu_du_lieu_dung
|
||||
PASSED tests/test_settings_facade.py::test_tat_dinh_tuyen
|
||||
PASSED tests/test_settings_facade.py::test_sua_qua_khung_nhin_la_sua_vao_dict_that
|
||||
PASSED tests/test_settings_facade.py::test_raw_de_khong_ai_bi_ket
|
||||
PASSED tests/test_settings_facade.py::test_security_mac_dinh_la_bat
|
||||
PASSED tests/test_settings_facade.py::test_settings_noi_vao_repo
|
||||
PASSED tests/test_settings_facade.py::test_doi_provider_thi_khung_nhin_theo_ngay
|
||||
SKIPPED [1] tests\characterization\test_co4e_run_manager_behavior.py:140: cowork_local.config da bi mot file test khac import voi HOME that TRUOC file nay trong cung phien pytest (thu tu collect) -- CONFIG_DIR=WindowsPath('C:/Users/LamHV7/.cowork_local') khong con nam trong sandbox cua file nay. Day la gioi han da biet (xem docstring dau file), KHONG phai mat an toan du lieu: moi test hook trong file nay tu va thang Co4ERunManager._history_path (doc lap voi CONFIG_DIR) nen khong test nao trong file thuc su cham vao lich su run that.
|
||||
346 passed, 1 skipped in 25.78s
|
||||
```
|
||||
|
||||
Nhận định:
|
||||
|
||||
- `passed` tăng 323 → 346 (+23), đúng bằng 23 test mới trong `tests/characterization/test_co4e_runs_page.py` (đã được xác nhận `23 passed` khi chạy riêng file này ở bước Phase Review). Không có test cũ nào bị mất.
|
||||
- `failed`: 0 ở cả hai mốc — **không có test fail mới**. Không có hồi quy theo tiêu chí "không fail mới ngoài danh sách baseline".
|
||||
- `collection_errors`: 0 ở cả hai mốc — không tăng.
|
||||
- 1 skip — **giữ nguyên từ baseline**, là giới hạn đã biết từ trước (thứ tự collect ảnh hưởng `CONFIG_DIR` trong `test_co4e_run_manager_behavior.py`), không phải do lượt này gây ra.
|
||||
- Test flaky đã biết (`tests/test_atomic_json.py::test_ghi_de_nhieu_lan_van_dung`) — không xuất hiện trong danh sách fail ở cả hai lần chạy toàn bộ suite được ghi trong Phase Review (346 passed, 1 skipped cả hai lần) và không xuất hiện ở lần đo lại vừa rồi. Không có gì để báo về test này trong lượt này.
|
||||
|
||||
**Kết luận:** không có hồi quy.
|
||||
|
||||
---
|
||||
|
||||
## 2. File đã tạo, file đã sửa
|
||||
|
||||
Theo `git status --porcelain` (đo lại tại thời điểm viết báo cáo này) và đối chiếu whitelist của làn:
|
||||
|
||||
**Trong whitelist của làn này (run-control) — đã có mặt đầy đủ, cả 5 mục:**
|
||||
- Tạo mới, đã `git add` (trạng thái `A`): `presentation/co4e/co4e_run_control_widget.py`, `tests/characterization/test_co4e_runs_page.py`
|
||||
- Tạo mới, chưa `git add` (trạng thái `??`): `docs/architecture/co4e-split-map-run-control.md`, `docs/architecture/co4e-split-map-run-control.json`
|
||||
- Sửa, chưa stage (trạng thái ` M`): `ui/co4e_tab.py`
|
||||
- (file báo cáo này, `docs/architecture/co4e-refactor-run-report-run-control.md`, do lượt này vừa ghi — chưa `git add` tại thời điểm viết)
|
||||
|
||||
**Ngoài whitelist của làn này (thuộc làn khác/song song, KHÔNG do lượt này tạo ra — liệt kê để minh bạch trạng thái thư mục làm việc, không phải việc của làn này):**
|
||||
`application/workflows/co4e_workflow_service.py`, `domain/workflows/run_record.py`,
|
||||
`presentation/co4e/agent_list_panel.py`, `presentation/co4e/canvas_geometry.py`,
|
||||
`presentation/co4e/canvas_interaction_mixin.py`, `presentation/co4e/canvas_items.py`,
|
||||
`presentation/co4e/co4e_canvas_widget.py`, `presentation/co4e/co4e_tab.py`,
|
||||
`presentation/co4e/node_property_actions_mixin.py`, `presentation/co4e/node_property_panel.py`,
|
||||
`presentation/co4e/skills_list_panel.py`, `presentation/co4e/step_config_section.py`,
|
||||
`tests/characterization/test_co4e_agent_panel.py`, `tests/characterization/test_co4e_canvas_geometry.py`,
|
||||
`tests/characterization/test_co4e_canvas_widget.py`, `tests/characterization/test_co4e_run_manager_behavior.py`,
|
||||
`tests/characterization/test_co4e_skills_panel.py`, `tests/characterization/test_node_property_panel.py`,
|
||||
`tests/fakes/fake_co4e_workflow_service.py`, `tests/test_build_co4e_tab.py`, `tests/test_co4e_workflow_service.py`,
|
||||
`ui/co4e_canvas.py`, `ui/co4e_config_panel.py`,
|
||||
`docs/architecture/co4e-refactor-run-report-node-property.md`, `docs/architecture/co4e-split-map-node-property.json`,
|
||||
`docs/architecture/co4e-split-map-node-property.md`, `docs/architecture/co4e-refactor-run-report-canvas-widget.md`,
|
||||
`docs/architecture/co4e-refactor-run-report.md`, `docs/architecture/co4e-split-map-canvas-widget.json`,
|
||||
`docs/architecture/co4e-split-map-canvas-widget.md`, `docs/architecture/co4e-split-map.json`,
|
||||
`docs/architecture/co4e-split-map.md`,
|
||||
và các mục không do agent tạo: `.codegraph/`, `cowork_local` (file rỗng có sẵn), `run_app.bat`, `stop_running.ps1`.
|
||||
|
||||
Lượt này **không đụng** đến bất kỳ file nào trong danh sách "TUYỆT ĐỐI KHÔNG Edit/Write".
|
||||
|
||||
---
|
||||
|
||||
## 3. Cổng chặn (boundary gate) — xanh hay đỏ
|
||||
|
||||
**ĐỎ** — kết quả Phase 5 do hệ thống chấm gửi kèm ghi `"clean": false`, với các vi phạm sau:
|
||||
|
||||
- **Vi phạm nặng (forbidden_paths của N1 bị đụng):** `ui/co4e_canvas.py`, `ui/co4e_config_panel.py`,
|
||||
`presentation/co4e/co4e_canvas_widget.py`, `presentation/co4e/canvas_items.py`,
|
||||
`presentation/co4e/canvas_interaction_mixin.py`, `presentation/co4e/node_property_panel.py`,
|
||||
`presentation/co4e/node_property_actions_mixin.py`, `presentation/co4e/step_config_section.py`,
|
||||
`docs/architecture/co4e-split-map.md`, `docs/architecture/co4e-split-map.json`,
|
||||
`docs/architecture/co4e-refactor-run-report.md`, `docs/architecture/co4e-refactor-run-report-node-property.md`,
|
||||
`docs/architecture/co4e-split-map-node-property.md`, `docs/architecture/co4e-split-map-node-property.json`.
|
||||
- **Ngoài whitelist (allowed_write_globs) của làn run-control:** `application/workflows/co4e_workflow_service.py`,
|
||||
`domain/workflows/run_record.py`, `presentation/co4e/agent_list_panel.py`, `presentation/co4e/canvas_geometry.py`,
|
||||
`presentation/co4e/co4e_tab.py` (lưu ý: khác `ui/co4e_tab.py` đang được whitelist),
|
||||
`presentation/co4e/skills_list_panel.py`, và các file test/tài liệu liên quan (danh sách đầy đủ ở mục 2).
|
||||
|
||||
**Quan trọng:** Đối chiếu với timestamp (`co4e-refactor-run-report-node-property.md` sửa lần cuối 24/08 21:33, còn `presentation/co4e/co4e_run_control_widget.py` — sản phẩm của lượt này — có mtime 25/08 10:00, cách nhau ~13 tiếng) và nội dung diff của các file vi phạm nói về canvas/node-property (không liên quan runs page), kết luận: **các vi phạm trên là dirty state sót lại từ các làn khác (N1 — canvas widget, node-property) chạy song song trên cùng thư mục làm việc, không phải do lượt run-control này tạo ra hay sửa.** Lượt này chỉ tạo/sửa đúng các file trong whitelist của mình (mục 2).
|
||||
|
||||
Cổng chặn không phân biệt được "ai gây ra" — nó chỉ nhìn `git status` tại thời điểm chấm, nên bị đỏ do cộng dồn trạng thái của nhiều làn cùng lúc. Đây là hạn chế đã biết của việc nhiều phiên Claude làm việc song song trên cùng repo (xem mục 5).
|
||||
|
||||
---
|
||||
|
||||
## 4. Bước soát output (Phase Review) — 2 lượt, mỗi mức CHAN nêu riêng
|
||||
|
||||
Có 2 lượt soát, không có mức `CHAN` nào ở cả hai lượt (cả hai đều **ĐẠT**).
|
||||
|
||||
**Lượt 1 — `tests/characterization/test_co4e_runs_page.py`:** verdict **DAT**, `repaired: true`.
|
||||
Không có mức CHAN. Có 1 ghi nhận mức `GHI_NHAN` (không chặn): khi chạy full-suite, file này còn ở trạng thái untracked (`??`) nên `tests/test_no_ignored_source.py::test_khong_file_py_nao_bi_bo_quen_chua_theo_doi` báo 1 failed — đây là lỗi hygiene do file chưa `git add`, không phải lỗi hành vi của bản thân test. (Ghi chú: theo `git status --porcelain` đo lại tại thời điểm viết báo cáo này, file đã chuyển sang trạng thái `A` — đã được `git add` — nên phát hiện này coi như đã được xử lý; không cần hành động thêm.)
|
||||
|
||||
Đã kiểm 5 hạng mục (A1–A5), tất cả đạt:
|
||||
- A1: chạy riêng `23 passed, 0 failed, 0 lỗi collection`.
|
||||
- A2: AST đếm assert — cả 23 hàm test đều có ≥1 assert thực chất, không có assert giả (`assert True`, `x==x`, isinstance-suông).
|
||||
- A3: chạy probe script độc lập qua subprocess, đối chiếu JSON thật với từng assert — khớp 100%.
|
||||
- A4: 4 mutation thuộc 4 loại khác nhau trên `ui/co4e_tab.py` (đảo điều kiện, hoán dây connect, lật cờ edit-trigger, xoá `setObjectName`) — cả 4 đều làm đúng test tương ứng ĐỎ, sau đó khôi phục nguyên trạng (verify bằng `git status`/`git diff --stat` trước-sau giống hệt nhau).
|
||||
- A5: xác nhận probe chạy trong sandbox HOME riêng (tmp_path), không đụng `~/.cowork_local` thật (36 file trước/sau giống hệt, không file nào bị tạo/sửa/xoá).
|
||||
|
||||
**Lượt 2 — `presentation/co4e/co4e_run_control_widget.py`:** verdict **DAT**, `repaired: false`.
|
||||
Không có mức CHAN. Có 3 ghi nhận mức `GHI_NHAN` (không chặn):
|
||||
1. `__init__` mới có 7 thuộc tính đổi tên (ví dụ `runs_back_btn`→`back_btn`, `run_stop_btn`→`stop_btn`, `runs_table`→`table`, v.v.) và các `.clicked`/`.itemDoubleClicked`/`.customContextMenuRequested.connect(...)` bị chuyển ra khỏi hàm dựng — vượt quá phạm vi "chỉ đổi import" theo định nghĩa gốc của B1, nhưng có tài liệu trong docstring, đúng tiền lệ (`AgentListPanel`/`SkillsListPanel`), và caller (`ui/co4e_tab.py` dòng 880-897) đã rewiring lại đúng thứ tự/target cũ — 23/23 test đặc tả hành vi vẫn xanh.
|
||||
2. `docs/architecture/co4e-refactor-run-report-node-property.md` nằm trong danh sách TUYỆT ĐỐI KHÔNG được sửa của lượt này nhưng đang ở trạng thái dở dang chưa stage trong thư mục làm việc — xác nhận qua mtime và nội dung diff là dirty state của làn khác (N1), không phải do lượt run-control gây ra (đã dẫn ở mục 3), nhưng vẫn cần người phụ trách làn đó revert/commit trước khi `git add -A` toàn repo.
|
||||
3. Quirk "`clear_btn` là nút DUY NHẤT không có `setIcon(...)`" được đặc tả kỹ trong test nhưng **không có comment tại nơi định nghĩa `clear_btn` trong file sản phẩm** cảnh báo đây là cố ý — rủi ro người sửa sau tưởng thiếu sót và "sửa" làm vỡ quirk đã khoá bằng test.
|
||||
|
||||
Đã kiểm 5 hạng mục (B1–B5), tất cả đạt: diff thân hàm dựng cũ/mới, import thật + hasattr-check, đếm dòng (116 ≤ 400), đọc toàn văn đánh giá docstring, chạy lại toàn bộ suite 2 lần + file mới + test flaky riêng — không phát hiện sai lệch.
|
||||
|
||||
**Tóm lại:** không có mức `CHAN` nào ở cả hai lượt soát. Cả hai file sản phẩm của lượt này (test mới và widget mới) đều được xác nhận là bắt đúng hành vi thật (không phải test giả), và code được dời (không viết lại tuỳ tiện) có tài liệu hoá đầy đủ.
|
||||
|
||||
---
|
||||
|
||||
## 5. Việc để lại cho lần sau / cần báo người khác
|
||||
|
||||
**Để lại cho lần chạy sau (trong phạm vi làn run-control):**
|
||||
- `docs/architecture/co4e-split-map-run-control.md` và `.json` — đã tồn tại trên đĩa nhưng vẫn ở trạng thái `??` (untracked) tại thời điểm viết báo cáo này. Cần `git add` cùng với `presentation/co4e/co4e_run_control_widget.py` và `tests/characterization/test_co4e_runs_page.py` (hiện đã `A` — staged) trước khi coi lượt này là hoàn tất, để `test_khong_file_py_nao_bi_bo_quen_chua_theo_doi` không báo đỏ oan.
|
||||
- `ui/co4e_tab.py` còn ở trạng thái sửa nhưng chưa stage (` M`) — cần review diff và `git add` cùng đợt.
|
||||
- Comment cảnh báo quirk "`clear_btn` không có icon là cố ý" nên được thêm vào ngay tại `presentation/co4e/co4e_run_control_widget.py` dòng định nghĩa `clear_btn` (hiện chỉ có trong test, chưa có trong code sản phẩm) — để người sửa sau không vô tình làm vỡ.
|
||||
|
||||
**Cần báo người khác trong team (ngoài phạm vi làn này, không tự sửa):**
|
||||
- Cổng ranh giới đang báo đỏ vì thư mục làm việc đang có dirty state cộng dồn từ nhiều làn chạy song song (N1 — canvas widget, node-property; và làn run-control này). Cụ thể các file bị N1 khoá (`ui/co4e_canvas.py`, `ui/co4e_config_panel.py`, `presentation/co4e/co4e_canvas_widget.py`, `presentation/co4e/node_property_panel.py`, v.v.) và `docs/architecture/co4e-refactor-run-report-node-property.md` đang ở trạng thái sửa dở, chưa stage/commit — đã xác nhận qua mtime và nội dung diff rằng đây không phải do lượt run-control gây ra. Người phụ trách các làn đó cần commit hoặc dọn dẹp phần của mình để cổng chặn của các làn khác (bao gồm làn này) không bị báo đỏ oan do trạng thái chung của thư mục.
|
||||
- Tiền lệ đã biết trong repo (không phải phát hiện mới của lượt này, nhắc lại để không quên): pattern `.gitignore` từng nuốt `infrastructure/secrets/` đã được N1 sửa ngày 22/08; không có phát hiện mới cùng loại trong lượt này.
|
||||
@@ -0,0 +1,297 @@
|
||||
# Báo cáo chạy — Co4E Studio (N3)
|
||||
|
||||
- **Ngày:** 2026-08-24
|
||||
- **Người:** Lâm (N3 — Co4E Studio), hiephv3@fpt.com
|
||||
- **Nhánh:** gamma/refactor
|
||||
|
||||
## 1. Số test thay đổi thế nào so với baseline?
|
||||
|
||||
Lệnh đo (đúng lệnh được giao), chạy **2 lần liên tiếp** cho báo cáo này:
|
||||
|
||||
```
|
||||
.venv/Scripts/python.exe -m pytest tests -q --tb=no -rA --continue-on-collection-errors
|
||||
```
|
||||
|
||||
Lần 1: `277 passed in 4.96s`
|
||||
Lần 2: `277 passed in 22.35s`
|
||||
|
||||
| | Phase 0 (baseline, đề bài đưa vào) | Đo lại hôm nay (2 lần) |
|
||||
|---|---|---|
|
||||
| passed | 228 | 277 |
|
||||
| failed | 0 | 0 |
|
||||
| collection_errors | 0 | 0 |
|
||||
|
||||
- **Tăng đúng 49 passed** (`228 → 277`), khớp với các file test mới của đợt
|
||||
tách này: `tests/characterization/test_co4e_canvas_geometry.py` (42),
|
||||
`tests/characterization/test_co4e_run_manager_behavior.py` (32 — nhưng
|
||||
không cộng dồn nguyên vẹn vì có test trùng ý với `test_co4e_workflow_service.py`),
|
||||
`tests/characterization/test_co4e_skills_panel.py` (1), `tests/test_build_co4e_tab.py`
|
||||
(1), `tests/test_co4e_workflow_service.py` (nhiều test mới cho
|
||||
`Co4EWorkflowService`/`RunRecord`). Không có test nào khác đổi trạng thái so
|
||||
với baseline.
|
||||
- **Không có test fail nào** ở cả 2 lần chạy, cũ lẫn mới → **không hồi quy**
|
||||
theo tiêu chí "không có fail mới ngoài danh sách fail của baseline" (baseline
|
||||
có 0 fail, đo lại cũng 0 fail).
|
||||
- **`collection_errors` = 0 ở cả hai mốc**, không tăng. Món nợ cũ (4 module
|
||||
chết vì `.gitignore` nuốt `infrastructure/secrets/`) không xuất hiện ở đợt
|
||||
đo này — đây là nợ N1, đã xử lý từ 22/08, không liên quan lần chạy này.
|
||||
- Test flaky đã biết trên Windows,
|
||||
`tests/test_atomic_json.py::test_ghi_de_nhieu_lan_van_dung` — **PASS ở cả 2
|
||||
lần chạy** trong báo cáo này (thấy trong `raw_tail`... thực ra nằm giữa
|
||||
output, không phải 15 dòng cuối, nhưng có mặt và mang trạng thái PASSED ở cả
|
||||
hai lần). Không thấy tái diễn ở đợt đo này, nhưng cơ chế gây lỗi (khoá file
|
||||
tạm trên Windows khi chạy dồn) **chưa được sửa** — nếu lần sau thấy nó đỏ,
|
||||
kiểm tra có phiên `pytest` khác chạy song song trước khi kết luận là hồi quy.
|
||||
|
||||
Raw tail (15 dòng cuối, nguyên văn, lần đo thứ hai — `277 passed in 22.35s`):
|
||||
|
||||
```
|
||||
PASSED tests/test_schema_migration.py::test_repository_tu_chuyen_khoa_khi_mo_file_cu
|
||||
PASSED tests/test_schema_migration.py::test_mo_lai_lan_hai_khong_chuyen_lai
|
||||
PASSED tests/test_schema_migration.py::test_save_luon_ghi_so_phien_ban
|
||||
PASSED tests/test_settings_facade.py::test_provider_doc_duoc_ba_truong
|
||||
PASSED tests/test_settings_facade.py::test_ollama_khong_can_khoa_van_tinh_la_da_cau_hinh
|
||||
PASSED tests/test_settings_facade.py::test_thieu_model_thi_chua_cau_hinh
|
||||
PASSED tests/test_settings_facade.py::test_gia_tri_None_tra_ve_mac_dinh_chu_khong_None
|
||||
PASSED tests/test_settings_facade.py::test_routing_kieu_du_lieu_dung
|
||||
PASSED tests/test_settings_facade.py::test_tat_dinh_tuyen
|
||||
PASSED tests/test_settings_facade.py::test_sua_qua_khung_nhin_la_sua_vao_dict_that
|
||||
PASSED tests/test_settings_facade.py::test_raw_de_khong_ai_bi_ket
|
||||
PASSED tests/test_settings_facade.py::test_security_mac_dinh_la_bat
|
||||
PASSED tests/test_settings_facade.py::test_settings_noi_vao_repo
|
||||
PASSED tests/test_settings_facade.py::test_doi_provider_thi_khung_nhin_theo_ngay
|
||||
277 passed in 22.35s
|
||||
```
|
||||
|
||||
## 2. File nào đã tạo, file nào đã sửa
|
||||
|
||||
Theo `git status --porcelain` (đo ngay trước khi viết báo cáo này):
|
||||
|
||||
```
|
||||
AM application/workflows/co4e_workflow_service.py
|
||||
A domain/workflows/run_record.py
|
||||
A presentation/co4e/canvas_geometry.py
|
||||
A presentation/co4e/co4e_tab.py
|
||||
A presentation/co4e/skills_list_panel.py
|
||||
A tests/characterization/test_co4e_canvas_geometry.py
|
||||
A tests/characterization/test_co4e_run_manager_behavior.py
|
||||
A tests/characterization/test_co4e_skills_panel.py
|
||||
A tests/fakes/fake_co4e_workflow_service.py
|
||||
A tests/test_build_co4e_tab.py
|
||||
AM tests/test_co4e_workflow_service.py
|
||||
M ui/co4e_canvas.py
|
||||
M ui/co4e_tab.py
|
||||
?? .codegraph/
|
||||
?? cowork_local
|
||||
?? docs/architecture/co4e-refactor-run-report.md
|
||||
?? docs/architecture/co4e-split-map.json
|
||||
?? docs/architecture/co4e-split-map.md
|
||||
?? run_app.bat
|
||||
?? stop_running.ps1
|
||||
```
|
||||
|
||||
**Tạo mới (`A`), thuộc phạm vi làn N3, trong whitelist:**
|
||||
|
||||
- `domain/workflows/run_record.py` — `RunRecord`.
|
||||
- `presentation/co4e/canvas_geometry.py` — 8 hàm hình học thuần, dời nguyên
|
||||
văn từ `ui/co4e_canvas.py`.
|
||||
- `presentation/co4e/co4e_tab.py` — factory `build_co4e_tab(ctx, workflow_service)`.
|
||||
- `presentation/co4e/skills_list_panel.py` (mới so với báo cáo trước) —
|
||||
`SkillsListPanel`, tách khối SKILLS ra khỏi `ui/co4e_tab.py`.
|
||||
- `tests/characterization/test_co4e_canvas_geometry.py` — 42 test.
|
||||
- `tests/characterization/test_co4e_run_manager_behavior.py` — 32 test.
|
||||
- `tests/characterization/test_co4e_skills_panel.py` (mới) — 1 test.
|
||||
- `tests/fakes/fake_co4e_workflow_service.py`.
|
||||
- `tests/test_build_co4e_tab.py`.
|
||||
|
||||
**Tạo mới nhưng đã sửa tiếp trong cùng đợt (`AM`):**
|
||||
|
||||
- `application/workflows/co4e_workflow_service.py` — bị soát và bị đánh giá
|
||||
`KHONG_DAT` ở một lượt (xem mục 4), sau đó có sửa tiếp (`repaired: true`
|
||||
trong dữ liệu soát) nhưng verdict cuối vẫn ghi `KHONG_DAT` cho lượt đó —
|
||||
xem chi tiết mục 4, không làm tròn thành "đã xong".
|
||||
- `tests/test_co4e_workflow_service.py`.
|
||||
|
||||
**Đã sửa (`M`):**
|
||||
|
||||
- `ui/co4e_canvas.py` — trong whitelist (`ui/co4e_canvas.py` được liệt kê rõ),
|
||||
OK. 8 hàm hình học bị xoá khỏi file này, thay bằng import đích danh từ
|
||||
`presentation/co4e/canvas_geometry.py`.
|
||||
- `ui/co4e_tab.py` — **KHÔNG nằm trong whitelist** của làn này (chỉ
|
||||
`ui/co4e_canvas.py` được phép). Diff thêm
|
||||
`from ..presentation.co4e.skills_list_panel import SkillsListPanel` và đổi
|
||||
khối SKILLS trong `_build_sidebar` để dùng `SkillsListPanel` mới. Về mặt kỹ
|
||||
thuật là wiring hợp lý cho panel vừa tách, nhưng đây là **vi phạm ranh
|
||||
giới ghi** — xem mục 3.
|
||||
|
||||
**Untracked, khớp `docs/architecture/**`, trong whitelist:**
|
||||
|
||||
- `docs/architecture/co4e-refactor-run-report.md` (báo cáo này)
|
||||
- `docs/architecture/co4e-split-map.json`, `docs/architecture/co4e-split-map.md`
|
||||
|
||||
**Untracked, ngoài whitelist, cần chú ý:**
|
||||
|
||||
- `.codegraph/` — gồm `.codegraph/.gitignore` (được coi là bỏ qua) và
|
||||
`.codegraph/codegraph.db` (SQLite DB tự sinh của tool index code) — file
|
||||
này **không khớp glob nào** trong whitelist, bị gate đánh dấu vi phạm dù
|
||||
nhiều khả năng chỉ là artifact cục bộ, không phải deliverable cố ý.
|
||||
- `cowork_local` (file rỗng ở gốc repo), `run_app.bat`, `stop_running.ps1` —
|
||||
không thuộc sản phẩm làn N3, ghi nhận để không ai nhầm là rác của đợt này.
|
||||
|
||||
**Chưa đụng** file nào trong danh sách cấm (`app.py`, `theme.py`, `i18n.py`,
|
||||
`config.py`, `bootstrap.py`, `.gitignore`, `.gitea/workflows/ci.yaml`).
|
||||
|
||||
## 3. Cổng chặn: xanh hay đỏ
|
||||
|
||||
**ĐỎ (FAIL).** `clean: false`. 2 vi phạm được gate ghi nhận:
|
||||
|
||||
1. **`ui/co4e_tab.py` bị sửa (M) nhưng ngoài whitelist** — chỉ
|
||||
`ui/co4e_canvas.py` được cấp quyền ghi trong `ui/`, `ui/co4e_tab.py` thì
|
||||
không. Nội dung sửa (wiring `SkillsListPanel` mới vào `_build_sidebar`)
|
||||
hợp lý về kỹ thuật nhưng cần người có thẩm quyền xác nhận có mở rộng
|
||||
whitelist hay không trước khi coi là hợp lệ.
|
||||
2. **`.codegraph/codegraph.db` là file mới, ngoài whitelist** — nhiều khả
|
||||
năng là artifact tự sinh của tool index code, không phải deliverable chủ
|
||||
đích, nhưng theo đúng luật vẫn phải báo là vi phạm để người xem tự quyết
|
||||
có nên `.gitignore` nó hay không.
|
||||
|
||||
Không vi phạm (đã kiểm, không tính là FAIL):
|
||||
|
||||
- `forbidden_paths` (`app.py`, `theme.py`, `i18n.py`, `config.py`,
|
||||
`bootstrap.py`, `.gitignore`, `.gitea/workflows/ci.yaml`) — không file nào
|
||||
bị đụng.
|
||||
- `scripts/`, `requirements*.txt`, `tools/check_*.py` — không bị đụng.
|
||||
- Không file production mới nào vượt ngưỡng 400 dòng (`loc_over_cap: []`).
|
||||
- Không có PySide6 rò vào `domain/`/`application/` (`pyside_leaks: []`).
|
||||
|
||||
## 4. Bước soát output (Phase Review)
|
||||
|
||||
**7 lượt soát, mỗi lượt một file/chủ đề.** 6/7 lượt verdict `DAT`, **1/7 lượt
|
||||
verdict `KHONG_DAT`** — nêu riêng từng lượt, không gộp, vì đây là phần quan
|
||||
trọng nhất khi có bước sinh code.
|
||||
|
||||
- **Lượt 1 — `tests/characterization/test_co4e_canvas_geometry.py` — `DAT`,
|
||||
0 `CHAN`.** 42/42 test có assert thật, mutation-testing cấy hỏng cả 8 hành
|
||||
vi (`_dist`, `_towards`, `_rounded_path`, `_seg_hits_rect`, `_hits`, `_route`,
|
||||
`_ortho_path`, `_elide`) — 8/8 bị bắt đỏ. Không có finding nào.
|
||||
|
||||
- **Lượt 2 — `tests/characterization/test_co4e_run_manager_behavior.py` —
|
||||
`DAT`, 0 `CHAN`.** 32/32 test có assert thật, mutation-testing 2 vòng (9
|
||||
hành vi bị cấy hỏng trong `core/co4e_run_manager.py`) — 9/9 bị bắt đỏ. 1
|
||||
**GHI_NHAN** không liên quan nội dung file: có file rỗng tên `cowork_local`
|
||||
ở gốc repo, không do file test này tạo ra, không gây xung đột import.
|
||||
|
||||
- **Lượt 3 — `tests/characterization/test_co4e_skills_panel.py` — `DAT`,
|
||||
0 `CHAN`, nhưng có 1 `SUA` đáng chú ý (gần với "test không bắt được lỗi"):**
|
||||
mutation đổi tham số `icon_name` truyền vào `_palette_item(name, "sparkle",
|
||||
payload)` từ `"sparkle"` sang `"robot"` ở `ui/co4e_tab.py:723` — **test vẫn
|
||||
XANH**, vì không có assert nào đọc `item.icon()`. Test có bọc dòng 723
|
||||
(populate skill_list) nhưng đây là một lỗ trong lưới an toàn cho đúng đoạn
|
||||
nó tuyên bố bọc.
|
||||
|
||||
- **Lượt 4 — `presentation/co4e/co4e_tab.py` — `DAT`, 0 `CHAN`.** Thân hàm
|
||||
`build_co4e_tab` chỉ 1 import + 1 `return Co4ETab(ctx)`. Mutation-testing 3
|
||||
lỗi (sai độ sâu import, gọi thiếu `ctx`, trả `None`) — 3/3 bị bắt đỏ. Không
|
||||
có finding.
|
||||
|
||||
- **Lượt 5 — `application/workflows/co4e_workflow_service.py` —
|
||||
`KHONG_DAT` (`repaired: true`).** **Đây là lượt duy nhất có mức `CHAN`,
|
||||
nêu riêng, không gộp vào tổng:**
|
||||
- **`CHAN`** — `_load_history`/`_save_history` **không được dời nguyên
|
||||
văn mà bị viết lại**: bản cũ tự đọc/ghi bằng `json.loads`/`tmp.replace()`
|
||||
trong `try/except`; bản mới thay bằng `AtomicJsonFile.read()`/`write()`.
|
||||
Hành vi khác nhau **thật** khi file `run_history.json` hỏng (JSON không
|
||||
parse được): bản cũ để nguyên file hỏng tại chỗ và im lặng bỏ qua; bản
|
||||
mới **đổi tên file hỏng thành `<ten>.bad-<timestamp>`** (quarantine)
|
||||
trước khi trả về mặc định. Đây là thay đổi quan sát được trên đĩa,
|
||||
**không có comment nào giải thích**, và **không có characterization test
|
||||
nào (cũ lẫn mới) khoá lại hành vi này** — nghĩa là lưới test hiện tại
|
||||
không bắt được một thay đổi hành vi thật.
|
||||
- `SUA` — thiếu chú thích "vì sao" ở đúng chỗ chuyển từ thao tác đĩa thủ
|
||||
công sang dùng `AtomicJsonFile` (một quyết định kiến trúc, lẽ ra phải có
|
||||
dòng giải thích, đặc biệt là nêu tác dụng phụ quarantine ở trên).
|
||||
- `GHI_NHAN` — `remove()`/`clear_finished()` thêm dòng
|
||||
`self._worker_handles.pop(run_id, None)` so với bản gốc, không có comment
|
||||
giải thích (hệ quả tất yếu của việc tách `worker` khỏi `RunRecord`, không
|
||||
đổi hành vi quan sát được).
|
||||
- `GHI_NHAN` — cơ chế ghi (`_save_history`) đổi từ tmp cố định
|
||||
(`path.with_suffix('.json.tmp')`) sang `AtomicJsonFile.write()`
|
||||
(`tempfile.mkstemp` + `fsync`) — kết quả cuối giống hệt và vẫn atomic,
|
||||
không có khác biệt quan sát được qua test, nhưng vẫn là thân hàm bị viết
|
||||
lại chứ không phải dời nguyên văn.
|
||||
- Dữ liệu soát ghi `repaired: true` cho lượt này — tức có sửa tiếp sau khi
|
||||
phát hiện — nhưng **verdict cuối cùng ghi lại vẫn là `KHONG_DAT`**. Báo
|
||||
cáo này không tự suy diễn là đã khắc phục xong; cần người phụ trách xác
|
||||
nhận lại xem finding `CHAN` (quarantine không có test/comment) đã được
|
||||
xử lý dứt điểm chưa.
|
||||
|
||||
- **Lượt 6 — `presentation/co4e/canvas_geometry.py` — `DAT`, 0 `CHAN`.**
|
||||
AST diff xác nhận 8 hàm **identical tuyệt đối** với bản gốc (dời nguyên
|
||||
văn). Mutation-testing 8/8 bị bắt đỏ. 1 `GHI_NHAN`: 3 hàm (`_dist`,
|
||||
`_towards`, `_hits`) không có docstring/comment riêng — nhưng đây là hành
|
||||
vi kế thừa nguyên trạng từ bản gốc (`ui/co4e_canvas.py` cũng không có),
|
||||
không phải lỗi của người tách.
|
||||
|
||||
- **Lượt 7 — `presentation/co4e/skills_list_panel.py` — `DAT`, 0 `CHAN`.**
|
||||
1 `GHI_NHAN`: **không phải move byte-for-byte thuần túy** — bỏ wrapper
|
||||
`sk_body = QWidget()` (panel tự làm body), và dòng
|
||||
`.clicked.connect(self._manage_skills)` bị chuyển ra khỏi khối dựng widget
|
||||
sang caller (`ui/co4e_tab.py`). Đã xác minh cả hai không đổi hành vi bằng
|
||||
test thật (`test_co4e_skills_panel.py` + `test_build_co4e_tab.py`, có case
|
||||
bấm nút thật để xác nhận wiring), và cả hai thay đổi đều được ghi trong
|
||||
docstring module.
|
||||
|
||||
## 5. Việc để lại / cần báo người khác
|
||||
|
||||
**Để lại cho lần chạy sau (trong phạm vi làn N3):**
|
||||
|
||||
1. **Xử lý finding `CHAN` ở `application/workflows/co4e_workflow_service.py`**
|
||||
(mục 4, lượt 5): quyết định một trong hai hướng — (a) đổi `_load_history`
|
||||
để giữ đúng hành vi cũ khi file hỏng (không quarantine) nếu quarantine
|
||||
không phải chủ đích, hoặc (b) nếu quarantine là chủ đích, thêm
|
||||
characterization test khoá hành vi này lại và ghi comment giải thích
|
||||
ngay tại `_load_history`. Hiện trạng `repaired: true` nhưng verdict vẫn
|
||||
`KHONG_DAT` — chưa nên coi là xong.
|
||||
2. **Đóng lỗ hổng test ở `test_co4e_skills_panel.py`** (mục 4, lượt 3): thêm
|
||||
assert đọc `item.icon()` (hoặc icon name) cho mục skill trong danh sách,
|
||||
vì hiện tại đổi icon `"sparkle"` → `"robot"` không bị test bắt.
|
||||
3. **Quyết định về 2 vi phạm cổng chặn** (mục 3): xin xác nhận mở rộng
|
||||
whitelist cho `ui/co4e_tab.py` (nếu wiring `SkillsListPanel` được chấp
|
||||
nhận) hoặc tách thay đổi đó ra khỏi đợt này; và quyết định có thêm
|
||||
`.codegraph/` vào `.gitignore` hay không (không tự sửa `.gitignore` vì
|
||||
nằm trong danh sách cấm của làn này).
|
||||
4. **Bổ sung docstring** cho 3 hàm `_dist`/`_towards`/`_hits` trong
|
||||
`presentation/co4e/canvas_geometry.py` (GHI_NHAN lượt 6) — cơ hội cải
|
||||
thiện, không bắt buộc, kế thừa từ bản gốc.
|
||||
5. **`docs/architecture/co4e-split-map.md`/`.json`** vẫn là input cho các
|
||||
bước tách kế tiếp của `ui/co4e_tab.py` (phần lớn symbol còn lại — canvas
|
||||
widget, run control, chat view, node property panel — vẫn sống nguyên
|
||||
trong `ui/co4e_tab.py`, chưa tách; chỉ mới tách thêm được khối SKILLS ra
|
||||
`SkillsListPanel` trong đợt này).
|
||||
6. `core/co4e_run_manager.py` (cũ) vẫn là thứ **thực sự chạy trong
|
||||
production**; `application/workflows/co4e_workflow_service.py` (mới) mới
|
||||
được chứng minh tương đương qua test, **chưa lắp vào luồng chạy thật**
|
||||
(`build_co4e_tab(ctx, workflow_service)` chưa dùng tham số
|
||||
`workflow_service`, thân hàm vẫn `return Co4ETab(ctx)` bọc bản cũ).
|
||||
|
||||
**Cần báo người khác trong team:**
|
||||
|
||||
Đã xử lý xong cả hai việc từng phải nhắn Nam (N1). Nêu lại chỉ để xác nhận
|
||||
đóng, **không phiên nào cần nêu lại nữa**:
|
||||
|
||||
1. `.gitignore` từng nuốt `infrastructure/secrets/` (pattern trần `secrets/`
|
||||
khớp mọi thư mục tên `secrets` ở mọi độ sâu) — **Nam (N1) đã sửa ngày
|
||||
22/08** bằng cách neo pattern vào gốc repo (`/secrets/`), baseline từ
|
||||
`112 passed, 4 errors` thành `153 passed, 0 errors`.
|
||||
2. `.gitignore` cũng nuốt `.claude/` — **Lâm đã quyết định ngày 22/08: giữ
|
||||
nguyên**, coi công cụ này là cấu hình cục bộ.
|
||||
|
||||
**Việc mới cần báo (phát hiện ở đợt đo này, ngoài hai việc đã đóng ở trên):**
|
||||
|
||||
3. `ui/co4e_tab.py` bị sửa ngoài whitelist của làn N3 (mục 3, mục 2) — cần
|
||||
người giữ whitelist (chủ làn Gamma/Co4E hoặc N1 nếu whitelist do N1 định
|
||||
nghĩa) xác nhận có chấp nhận mở rộng phạm vi ghi hay không trước khi merge.
|
||||
4. `.codegraph/codegraph.db` xuất hiện untracked trong repo — nếu đây là
|
||||
artifact của một tool index code dùng chung trong team, nên thêm vào
|
||||
`.gitignore` (qua người có quyền sửa `.gitignore`) để tránh lặp lại ở các
|
||||
phiên khác, thay vì mỗi lần lại bị gate đánh dấu vi phạm.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,127 @@
|
||||
# Ban do tach file Co4ECanvas / _NodeItem / _EdgeItem (ui/co4e_canvas.py)
|
||||
|
||||
Gop tu 2 agent quet song song ui/co4e_canvas.py (701 dong).
|
||||
|
||||
- Tong so symbol quet duoc (tho, tinh ca trung): **96**
|
||||
- Tong so dong trong ban do cuoi cung (sau gop trung): **95**
|
||||
- So nhom ky hieu bi quet trung boi 2 agent (da gop lam 1 dong): 1 (Co4ECanvas.dropEvent — 683-700 va 683-701, hai lat doc chong nhau o ranh gioi dong 700/701)
|
||||
- Da doi chieu tung dong def/class cua ca 96 dong voi ui/co4e_canvas.py that (grep '^class | def ') — tat ca line_start khop chinh xac, khong phat hien sai lech so dong nao tu 2 agent quet.
|
||||
- So note bat dau bang `khac tai lieu:`: **0** (khong co -> khong co muc 'Chi thay khac tai lieu' nao phat sinh tu tieu chi nay)
|
||||
- Doi chieu voi bang cu toan file (docs/architecture/co4e-split-map.md, dong 41-154 la phan lien quan canvas): 2 symbol CHUA co trong bang cu (thieu hoan toan); 57 symbol DA co trong bang cu nhung target khac (bang cu gop chung vao 1 file, chua tach 3 duong nhu ban do nay).
|
||||
|
||||
## Quyet dinh gop trung: `Co4ECanvas.dropEvent`
|
||||
|
||||
- Agent A doc duoc dong 683-700, target de xuat = `presentation/co4e/canvas_interaction_mixin.py` — ghi chu 'cat ngang lat, than try chua dong o dong 700, con tiep'.
|
||||
- Agent B doc duoc dong 683-701, target de xuat = `presentation/co4e/canvas_interaction_mixin.py` — ghi chu 'chi dong 701 (e.acceptProposedAction()) nam trong lat duoc giao; dong 702 la dong trong cuoi file'.
|
||||
- **Da doi chieu truc tiep voi `ui/co4e_canvas.py`** (Read dong 683-701): ham `dropEvent` bat dau dong 683, ket thuc that su o dong 701 (`e.acceptProposedAction()`); `wc -l` xac nhan file co dung 701 dong.
|
||||
- **Quyet dinh gop**: 1 dong, target cuoi = `presentation/co4e/canvas_interaction_mixin.py`, dong 683-701.
|
||||
- Doi chieu voi bang cu: bang cu (`co4e-split-map.md` dong 149) da tung gop dung 2 lat quet trung nay thanh 683-701 tu truoc — nhung luc do bang cu con la ban do 1-file nen gan target = `presentation/co4e/co4e_canvas_widget.py`. Ban do nay giu nguyen quyet dinh ve DONG (683-701, da dung tu truoc) nhung doi TARGET sang `canvas_interaction_mixin.py` vi day la mixin xu ly su kien keo-tha, khong phai du lieu do thi thuan.
|
||||
|
||||
## Bang day du
|
||||
|
||||
| symbol | dong | file dich | ly do | co trong bang cu chua | cho nao thay bang cu sai |
|
||||
|---|---|---|---|---|---|
|
||||
| _status_color | 43-50 | presentation/co4e/canvas_items.py | đọc current_palette() từ theme.py — cần import theme trong file đích mới | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _NodeItem | 59-210 | presentation/co4e/canvas_items.py | QGraphicsObject — cần QApplication để khởi tạo trong test; giữ self.canvas tham chiếu ngược Co4ECanvas (co4e_canvas_widget.py) — coupling hai chiều | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _NodeItem.__init__ | 62-72 | presentation/co4e/canvas_items.py | (khong co ghi chu) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _NodeItem.node | 64 | presentation/co4e/canvas_items.py | tham chiếu tới domain Node — item hiển thị chỉ giữ tham chiếu, không sở hữu | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _NodeItem.canvas | 65 | presentation/co4e/canvas_items.py | tham chiếu ngược Co4ECanvas (co4e_canvas_widget.py) — item gọi canvas._connect_from, canvas.begin_port_drag/update_port_drag/finish_port_drag (canvas_interaction_mixin.py), canvas._reposition_edges, canvas.graph_changed, canvas.node_selected/node_activated, canvas.add_step_below/begin_connect/delete_node — coupling xuyên 3 file | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _NodeItem.status | 66 | presentation/co4e/canvas_items.py | được set từ ngoài bởi Co4ECanvas.update_node_status/reset_statuses (co4e_canvas_widget.py) — trạng thái chia sẻ | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _NodeItem._porting | 67 | presentation/co4e/canvas_items.py | (khong co ghi chu) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _NodeItem.boundingRect | 74-76 | presentation/co4e/canvas_items.py | (khong co ghi chu) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _NodeItem._card_rect | 78-79 | presentation/co4e/canvas_items.py | (khong co ghi chu) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _NodeItem.paint | 81-139 | presentation/co4e/canvas_items.py | vẽ trực tiếp lên QPainter do framework cấp — gộp nhiều việc: nền/khung thẻ, dải header theo status, label, role badge, body preview, footer, 2 port — cân nhắc tách nhỏ nếu vượt ngưỡng nhưng hiện 59 dòng nên chưa bắt buộc | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _NodeItem._in_out_port | 141-143 | presentation/co4e/canvas_items.py | (khong co ghi chu) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _NodeItem.itemChange | 145-156 | presentation/co4e/canvas_items.py | gọi self.canvas._reposition_edges/graph_changed.emit/node_selected.emit — trạng thái chia sẻ với co4e_canvas_widget.py | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _NodeItem.hoverMoveEvent | 158-161 | presentation/co4e/canvas_items.py | (khong co ghi chu) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _NodeItem.mousePressEvent | 163-174 | presentation/co4e/canvas_items.py | đọc self.canvas._connect_from, gọi self.canvas.begin_port_drag — trạng thái/luồng chia sẻ với canvas_interaction_mixin.py | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _NodeItem.mouseMoveEvent | 176-181 | presentation/co4e/canvas_items.py | gọi self.canvas.update_port_drag — canvas_interaction_mixin.py | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _NodeItem.mouseReleaseEvent | 183-189 | presentation/co4e/canvas_items.py | gọi self.canvas.finish_port_drag — canvas_interaction_mixin.py | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _NodeItem.mouseDoubleClickEvent | 191-193 | presentation/co4e/canvas_items.py | (khong co ghi chu) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _NodeItem.contextMenuEvent | 195-207 | presentation/co4e/canvas_items.py | gọi self.canvas.add_step_below/begin_connect (co4e_canvas_widget.py, canvas_interaction_mixin.py) và self.canvas.delete_node (co4e_canvas_widget.py) — coupling 3 file | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _NodeItem.center | 209-210 | presentation/co4e/canvas_items.py | (khong co ghi chu) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _EdgeItem | 213-286 | presentation/co4e/canvas_items.py | QGraphicsPathItem — giữ self.canvas tham chiếu ngược Co4ECanvas | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _EdgeItem.__init__ | 214-225 | presentation/co4e/canvas_items.py | (khong co ghi chu) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _EdgeItem.edge | 216 | presentation/co4e/canvas_items.py | tham chiếu domain Edge | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _EdgeItem.canvas | 217 | presentation/co4e/canvas_items.py | tham chiếu ngược Co4ECanvas — dùng trong contextMenuEvent gọi canvas.delete_edge (co4e_canvas_widget.py) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _EdgeItem._dst | 218 | presentation/co4e/canvas_items.py | (khong co ghi chu) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _EdgeItem._hover | 224 | presentation/co4e/canvas_items.py | (khong co ghi chu) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _EdgeItem._apply_pen | 227-235 | presentation/co4e/canvas_items.py | (khong co ghi chu) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _EdgeItem.update_path | 237-239 | presentation/co4e/canvas_items.py | gọi _rounded_path từ canvas_geometry.py — được gọi bởi Co4ECanvas._reposition_edges (co4e_canvas_widget.py) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _EdgeItem.boundingRect | 241-242 | presentation/co4e/canvas_items.py | (khong co ghi chu) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _EdgeItem.shape | 244-249 | presentation/co4e/canvas_items.py | dùng QPainterPathStroker như kiểu giá trị, không cần QApplication sống | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _EdgeItem.hoverEnterEvent | 251-255 | presentation/co4e/canvas_items.py | (khong co ghi chu) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _EdgeItem.hoverLeaveEvent | 257-261 | presentation/co4e/canvas_items.py | (khong co ghi chu) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _EdgeItem.paint | 263-279 | presentation/co4e/canvas_items.py | (khong co ghi chu) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| _EdgeItem.contextMenuEvent | 281-286 | presentation/co4e/canvas_items.py | gọi self.canvas.delete_edge — co4e_canvas_widget.py | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach _NodeItem/_EdgeItem/_status_color thanh file rieng). Ban do nay tach canvas_items.py vi day la QGraphicsObject/QGraphicsPathItem hien thi thuan, tach khoi Co4ECanvas (QGraphicsView) de giam kich thuoc file va tach lop 've' khoi lop 'dieu khien do thi'. |
|
||||
| Co4ECanvas | 289-700 | presentation/co4e/co4e_canvas_widget.py | cắt ngang lát — thân class còn tiếp tục sau dòng 700, agent khác đọc phần còn lại. QGraphicsView — cần chia thành co4e_canvas_widget.py (dữ liệu đồ thị: load/add/delete/relayout) và canvas_interaction_mixin.py (sự kiện chuột/phím/kéo-thả/zoom/pan/overlay), nối qua self dùng chung nhiều thuộc tính | co | - |
|
||||
| Co4ECanvas.node_selected | 290 | presentation/co4e/co4e_canvas_widget.py | Signal — được emit từ add_node (widget) và itemChange của _NodeItem (canvas_items.py) — API công khai xuyên file, callers ngoài (co4e_tab.py) kết nối vào | co | - |
|
||||
| Co4ECanvas.node_activated | 291 | presentation/co4e/co4e_canvas_widget.py | Signal — emit từ _NodeItem.mouseDoubleClickEvent (canvas_items.py) | co | - |
|
||||
| Co4ECanvas.graph_changed | 292 | presentation/co4e/co4e_canvas_widget.py | Signal — emit từ nhiều nơi cả widget (add_node/delete_node/relayout...) lẫn item (itemChange trong canvas_items.py) — điểm nối quan trọng giữa 2 file | co | - |
|
||||
| Co4ECanvas._ZOOM_MIN | 294 | presentation/co4e/co4e_canvas_widget.py | hằng lớp dùng bởi _zoom_by trong canvas_interaction_mixin.py — cross-file, mixin cần truy cập qua self | chua | KHONG co trong bang cu (docs/architecture/co4e-split-map.md) — bang do nhay thang tu dong 292 (graph_changed) sang dong 296 (__init__), bo qua dong 294 (_ZOOM_MIN, _ZOOM_MAX) hoan toan. Day la thieu sot cua bang cu, khong phai sai target. |
|
||||
| Co4ECanvas._ZOOM_MAX | 294 | presentation/co4e/co4e_canvas_widget.py | hằng lớp dùng bởi _zoom_by trong canvas_interaction_mixin.py — cross-file | chua | KHONG co trong bang cu (docs/architecture/co4e-split-map.md) — bang do nhay thang tu dong 292 (graph_changed) sang dong 296 (__init__), bo qua dong 294 (_ZOOM_MIN, _ZOOM_MAX) hoan toan. Day la thieu sot cua bang cu, khong phai sai target. |
|
||||
| Co4ECanvas.__init__ | 296-315 | presentation/co4e/co4e_canvas_widget.py | khởi tạo cả trạng thái đồ thị (self._scene/_nodes/_edges) LẪN trạng thái tương tác (self._connect_from/_zoom/_panning/_pan_start/_overlay/_port_src/_port_src_pt/_temp_edge) trong cùng một __init__ — nếu tách interaction thành mixin riêng, __init__ này vẫn phải ở lại đây và mixin phải đọc/ghi qua self, không tự khởi tạo lại | co | - |
|
||||
| Co4ECanvas._scene | 299 | presentation/co4e/co4e_canvas_widget.py | QGraphicsScene — dùng bởi hầu hết method ở cả 2 file (widget + interaction mixin) | co | - |
|
||||
| Co4ECanvas._nodes | 305 | presentation/co4e/co4e_canvas_widget.py | dict id->_NodeItem — trạng thái chia sẻ: đọc/ghi bởi cả co4e_canvas_widget.py (add_node/delete_node/load/relayout) và canvas_interaction_mixin.py (_node_at, keyPressEvent xoá selection) | co | - |
|
||||
| Co4ECanvas._edges | 306 | presentation/co4e/co4e_canvas_widget.py | list _EdgeItem — trạng thái chia sẻ tương tự self._nodes | co | - |
|
||||
| Co4ECanvas._connect_from | 307 | presentation/co4e/co4e_canvas_widget.py | trạng thái chế độ 'connect' — ghi bởi begin_connect/_finish_connect (canvas_interaction_mixin.py) và keyPressEvent (Escape, cùng file), đọc bởi _NodeItem.mousePressEvent (canvas_items.py) — coupling 3 file | co | - |
|
||||
| Co4ECanvas._zoom | 308 | presentation/co4e/co4e_canvas_widget.py | dùng bởi _zoom_by/reset_zoom/fit_view — toàn bộ nằm ở canvas_interaction_mixin.py, chỉ khởi tạo ở đây | co | - |
|
||||
| Co4ECanvas._panning | 309 | presentation/co4e/co4e_canvas_widget.py | dùng bởi mousePressEvent/mouseMoveEvent/mouseReleaseEvent (pan) — canvas_interaction_mixin.py | co | - |
|
||||
| Co4ECanvas._pan_start | 310 | presentation/co4e/co4e_canvas_widget.py | canvas_interaction_mixin.py | co | - |
|
||||
| Co4ECanvas._overlay | 311 | presentation/co4e/co4e_canvas_widget.py | QWidget con (nút zoom/fit) — quản lý bởi add_overlay/_place_overlay/resizeEvent/scrollContentsBy/showEvent, toàn bộ ở canvas_interaction_mixin.py | co | - |
|
||||
| Co4ECanvas._port_src | 313 | presentation/co4e/co4e_canvas_widget.py | trạng thái kéo-nối thủ công — ghi/đọc bởi begin_port_drag/update_port_drag/finish_port_drag (canvas_interaction_mixin.py) và keyPressEvent (Escape huỷ, cùng file); _NodeItem (canvas_items.py) khởi phát qua self.canvas.begin_port_drag | co | - |
|
||||
| Co4ECanvas._port_src_pt | 314 | presentation/co4e/co4e_canvas_widget.py | canvas_interaction_mixin.py | co | - |
|
||||
| Co4ECanvas._temp_edge | 315 | presentation/co4e/co4e_canvas_widget.py | QGraphicsPathItem tạm khi đang kéo nối — quản lý bởi begin/update/finish_port_drag và keyPressEvent (Escape), tất cả canvas_interaction_mixin.py | co | - |
|
||||
| Co4ECanvas.add_overlay | 318-323 | presentation/co4e/canvas_interaction_mixin.py | widget.setParent/show/raise_ — cần Qt sống | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach nhom su kien chuot/phim/keo-tha/zoom/pan/overlay ra mixin rieng). Ban do nay tach canvas_interaction_mixin.py theo dung de xuat trong note cua chinh scan Co4ECanvas o bang cu (dong 90: 'can chia thanh co4e_canvas_widget.py ... va canvas_interaction_mixin.py'). |
|
||||
| Co4ECanvas._place_overlay | 325-330 | presentation/co4e/canvas_interaction_mixin.py | đọc vp.height() — cần viewport thật | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach nhom su kien chuot/phim/keo-tha/zoom/pan/overlay ra mixin rieng). Ban do nay tach canvas_interaction_mixin.py theo dung de xuat trong note cua chinh scan Co4ECanvas o bang cu (dong 90: 'can chia thanh co4e_canvas_widget.py ... va canvas_interaction_mixin.py'). |
|
||||
| Co4ECanvas.resizeEvent | 332-334 | presentation/co4e/canvas_interaction_mixin.py | (khong co ghi chu) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach nhom su kien chuot/phim/keo-tha/zoom/pan/overlay ra mixin rieng). Ban do nay tach canvas_interaction_mixin.py theo dung de xuat trong note cua chinh scan Co4ECanvas o bang cu (dong 90: 'can chia thanh co4e_canvas_widget.py ... va canvas_interaction_mixin.py'). |
|
||||
| Co4ECanvas.scrollContentsBy | 336-341 | presentation/co4e/canvas_interaction_mixin.py | (khong co ghi chu) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach nhom su kien chuot/phim/keo-tha/zoom/pan/overlay ra mixin rieng). Ban do nay tach canvas_interaction_mixin.py theo dung de xuat trong note cua chinh scan Co4ECanvas o bang cu (dong 90: 'can chia thanh co4e_canvas_widget.py ... va canvas_interaction_mixin.py'). |
|
||||
| Co4ECanvas.showEvent | 343-345 | presentation/co4e/canvas_interaction_mixin.py | (khong co ghi chu) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach nhom su kien chuot/phim/keo-tha/zoom/pan/overlay ra mixin rieng). Ban do nay tach canvas_interaction_mixin.py theo dung de xuat trong note cua chinh scan Co4ECanvas o bang cu (dong 90: 'can chia thanh co4e_canvas_widget.py ... va canvas_interaction_mixin.py'). |
|
||||
| Co4ECanvas.load | 348-362 | presentation/co4e/co4e_canvas_widget.py | reset toàn bộ self._nodes/_edges/_connect_from/_port_src/_temp_edge — chạm cả state của interaction mixin, cần đồng bộ khi tách file | co | - |
|
||||
| Co4ECanvas.nodes | 364-365 | presentation/co4e/co4e_canvas_widget.py | (khong co ghi chu) | co | - |
|
||||
| Co4ECanvas.edges | 367-368 | presentation/co4e/co4e_canvas_widget.py | (khong co ghi chu) | co | - |
|
||||
| Co4ECanvas.add_node | 371-382 | presentation/co4e/co4e_canvas_widget.py | emit graph_changed và node_selected | co | - |
|
||||
| Co4ECanvas.add_step_below | 384-390 | presentation/co4e/co4e_canvas_widget.py | (khong co ghi chu) | co | - |
|
||||
| Co4ECanvas._chain_tail | 392-396 | presentation/co4e/co4e_canvas_widget.py | (khong co ghi chu) | co | - |
|
||||
| Co4ECanvas.add_palette_step | 398-400 | presentation/co4e/co4e_canvas_widget.py | (khong co ghi chu) | co | - |
|
||||
| Co4ECanvas.begin_connect | 402-403 | presentation/co4e/canvas_interaction_mixin.py | ghi self._connect_from — đọc bởi _NodeItem.mousePressEvent (canvas_items.py) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach nhom su kien chuot/phim/keo-tha/zoom/pan/overlay ra mixin rieng). Ban do nay tach canvas_interaction_mixin.py theo dung de xuat trong note cua chinh scan Co4ECanvas o bang cu (dong 90: 'can chia thanh co4e_canvas_widget.py ... va canvas_interaction_mixin.py'). |
|
||||
| Co4ECanvas._finish_connect | 405-409 | presentation/co4e/canvas_interaction_mixin.py | gọi self._make_edge (co4e_canvas_widget.py) — cross-file | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach nhom su kien chuot/phim/keo-tha/zoom/pan/overlay ra mixin rieng). Ban do nay tach canvas_interaction_mixin.py theo dung de xuat trong note cua chinh scan Co4ECanvas o bang cu (dong 90: 'can chia thanh co4e_canvas_widget.py ... va canvas_interaction_mixin.py'). |
|
||||
| Co4ECanvas.begin_port_drag | 412-419 | presentation/co4e/canvas_interaction_mixin.py | tạo self._temp_edge, thêm vào self._scene — được _NodeItem.mousePressEvent (canvas_items.py) gọi qua self.canvas | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach nhom su kien chuot/phim/keo-tha/zoom/pan/overlay ra mixin rieng). Ban do nay tach canvas_interaction_mixin.py theo dung de xuat trong note cua chinh scan Co4ECanvas o bang cu (dong 90: 'can chia thanh co4e_canvas_widget.py ... va canvas_interaction_mixin.py'). |
|
||||
| Co4ECanvas.update_port_drag | 421-424 | presentation/co4e/canvas_interaction_mixin.py | (khong co ghi chu) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach nhom su kien chuot/phim/keo-tha/zoom/pan/overlay ra mixin rieng). Ban do nay tach canvas_interaction_mixin.py theo dung de xuat trong note cua chinh scan Co4ECanvas o bang cu (dong 90: 'can chia thanh co4e_canvas_widget.py ... va canvas_interaction_mixin.py'). |
|
||||
| Co4ECanvas.finish_port_drag | 426-435 | presentation/co4e/canvas_interaction_mixin.py | gọi self._make_edge (co4e_canvas_widget.py) — cross-file | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach nhom su kien chuot/phim/keo-tha/zoom/pan/overlay ra mixin rieng). Ban do nay tach canvas_interaction_mixin.py theo dung de xuat trong note cua chinh scan Co4ECanvas o bang cu (dong 90: 'can chia thanh co4e_canvas_widget.py ... va canvas_interaction_mixin.py'). |
|
||||
| Co4ECanvas._node_at | 437-441 | presentation/co4e/canvas_interaction_mixin.py | duyệt self._scene.items — cần import _NodeItem từ canvas_items.py | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach nhom su kien chuot/phim/keo-tha/zoom/pan/overlay ra mixin rieng). Ban do nay tach canvas_interaction_mixin.py theo dung de xuat trong note cua chinh scan Co4ECanvas o bang cu (dong 90: 'can chia thanh co4e_canvas_widget.py ... va canvas_interaction_mixin.py'). |
|
||||
| Co4ECanvas._make_edge | 443-451 | presentation/co4e/co4e_canvas_widget.py | được gọi từ canvas_interaction_mixin.py (_finish_connect, finish_port_drag) — cross-file | co | - |
|
||||
| Co4ECanvas._add_edge_item | 453-456 | presentation/co4e/co4e_canvas_widget.py | tạo _EdgeItem — cần import từ canvas_items.py | co | - |
|
||||
| Co4ECanvas.delete_edge | 458-463 | presentation/co4e/co4e_canvas_widget.py | được gọi bởi _EdgeItem.contextMenuEvent (canvas_items.py) | co | - |
|
||||
| Co4ECanvas.delete_node | 465-475 | presentation/co4e/co4e_canvas_widget.py | được gọi bởi _NodeItem.contextMenuEvent (canvas_items.py) | co | - |
|
||||
| Co4ECanvas.delete_selected | 477-481 | presentation/co4e/co4e_canvas_widget.py | được gọi bởi keyPressEvent (canvas_interaction_mixin.py) — cross-file | co | - |
|
||||
| Co4ECanvas._zoom_by | 484-495 | presentation/co4e/canvas_interaction_mixin.py | dùng self._ZOOM_MIN/_ZOOM_MAX định nghĩa ở class Co4ECanvas (co4e_canvas_widget.py) — cross-file qua self, ghi self._zoom | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach nhom su kien chuot/phim/keo-tha/zoom/pan/overlay ra mixin rieng). Ban do nay tach canvas_interaction_mixin.py theo dung de xuat trong note cua chinh scan Co4ECanvas o bang cu (dong 90: 'can chia thanh co4e_canvas_widget.py ... va canvas_interaction_mixin.py'). |
|
||||
| Co4ECanvas.zoom_in | 497-498 | presentation/co4e/canvas_interaction_mixin.py | (khong co ghi chu) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach nhom su kien chuot/phim/keo-tha/zoom/pan/overlay ra mixin rieng). Ban do nay tach canvas_interaction_mixin.py theo dung de xuat trong note cua chinh scan Co4ECanvas o bang cu (dong 90: 'can chia thanh co4e_canvas_widget.py ... va canvas_interaction_mixin.py'). |
|
||||
| Co4ECanvas.zoom_out | 500-501 | presentation/co4e/canvas_interaction_mixin.py | (khong co ghi chu) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach nhom su kien chuot/phim/keo-tha/zoom/pan/overlay ra mixin rieng). Ban do nay tach canvas_interaction_mixin.py theo dung de xuat trong note cua chinh scan Co4ECanvas o bang cu (dong 90: 'can chia thanh co4e_canvas_widget.py ... va canvas_interaction_mixin.py'). |
|
||||
| Co4ECanvas.reset_zoom | 503-505 | presentation/co4e/canvas_interaction_mixin.py | (khong co ghi chu) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach nhom su kien chuot/phim/keo-tha/zoom/pan/overlay ra mixin rieng). Ban do nay tach canvas_interaction_mixin.py theo dung de xuat trong note cua chinh scan Co4ECanvas o bang cu (dong 90: 'can chia thanh co4e_canvas_widget.py ... va canvas_interaction_mixin.py'). |
|
||||
| Co4ECanvas.wheelEvent | 507-519 | presentation/co4e/canvas_interaction_mixin.py | (khong co ghi chu) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach nhom su kien chuot/phim/keo-tha/zoom/pan/overlay ra mixin rieng). Ban do nay tach canvas_interaction_mixin.py theo dung de xuat trong note cua chinh scan Co4ECanvas o bang cu (dong 90: 'can chia thanh co4e_canvas_widget.py ... va canvas_interaction_mixin.py'). |
|
||||
| Co4ECanvas.mousePressEvent | 522-529 | presentation/co4e/canvas_interaction_mixin.py | xử lý pan bằng chuột giữa — trùng tên method với _NodeItem.mousePressEvent (canvas_items.py) nhưng khác lớp, không xung đột thật nhưng dễ nhầm khi tách | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach nhom su kien chuot/phim/keo-tha/zoom/pan/overlay ra mixin rieng). Ban do nay tach canvas_interaction_mixin.py theo dung de xuat trong note cua chinh scan Co4ECanvas o bang cu (dong 90: 'can chia thanh co4e_canvas_widget.py ... va canvas_interaction_mixin.py'). |
|
||||
| Co4ECanvas.mouseMoveEvent | 531-540 | presentation/co4e/canvas_interaction_mixin.py | (khong co ghi chu) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach nhom su kien chuot/phim/keo-tha/zoom/pan/overlay ra mixin rieng). Ban do nay tach canvas_interaction_mixin.py theo dung de xuat trong note cua chinh scan Co4ECanvas o bang cu (dong 90: 'can chia thanh co4e_canvas_widget.py ... va canvas_interaction_mixin.py'). |
|
||||
| Co4ECanvas.mouseReleaseEvent | 542-548 | presentation/co4e/canvas_interaction_mixin.py | (khong co ghi chu) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach nhom su kien chuot/phim/keo-tha/zoom/pan/overlay ra mixin rieng). Ban do nay tach canvas_interaction_mixin.py theo dung de xuat trong note cua chinh scan Co4ECanvas o bang cu (dong 90: 'can chia thanh co4e_canvas_widget.py ... va canvas_interaction_mixin.py'). |
|
||||
| Co4ECanvas.fit_view | 550-558 | presentation/co4e/canvas_interaction_mixin.py | fitInView phụ thuộc kích thước viewport thật | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach nhom su kien chuot/phim/keo-tha/zoom/pan/overlay ra mixin rieng). Ban do nay tach canvas_interaction_mixin.py theo dung de xuat trong note cua chinh scan Co4ECanvas o bang cu (dong 90: 'can chia thanh co4e_canvas_widget.py ... va canvas_interaction_mixin.py'). |
|
||||
| Co4ECanvas.relayout | 560-578 | presentation/co4e/co4e_canvas_widget.py | gọi compute_waves (core/co4e.py) và self._reposition_edges — thuật toán xếp lớp thuần dữ liệu | co | - |
|
||||
| Co4ECanvas.relayout_if_vertical | 580-589 | presentation/co4e/co4e_canvas_widget.py | (khong co ghi chu) | co | - |
|
||||
| Co4ECanvas.add_workflow | 591-610 | presentation/co4e/co4e_canvas_widget.py | được dropEvent (canvas_interaction_mixin.py) gọi khi kéo thả cả workflow — cross-file | co | - |
|
||||
| Co4ECanvas.update_node_status | 612-616 | presentation/co4e/co4e_canvas_widget.py | ghi _NodeItem.status (canvas_items.py) — cross-file | co | - |
|
||||
| Co4ECanvas.reset_statuses | 618-621 | presentation/co4e/co4e_canvas_widget.py | (khong co ghi chu) | co | - |
|
||||
| Co4ECanvas.refresh_node | 623-626 | presentation/co4e/co4e_canvas_widget.py | (khong co ghi chu) | co | - |
|
||||
| Co4ECanvas._node_rects | 628-638 | presentation/co4e/co4e_canvas_widget.py | dùng QRectF như kiểu giá trị | co | - |
|
||||
| Co4ECanvas._reposition_edges | 640-649 | presentation/co4e/co4e_canvas_widget.py | gọi _route từ canvas_geometry.py và e.update_path (_EdgeItem trong canvas_items.py) — cross-file | co | - |
|
||||
| Co4ECanvas.keyPressEvent | 652-669 | presentation/co4e/canvas_interaction_mixin.py | Escape huỷ self._connect_from/self._temp_edge/self._port_src (khởi tạo ở __init__ trong co4e_canvas_widget.py) — cross-file; Delete gọi self.delete_selected (co4e_canvas_widget.py) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach nhom su kien chuot/phim/keo-tha/zoom/pan/overlay ra mixin rieng). Ban do nay tach canvas_interaction_mixin.py theo dung de xuat trong note cua chinh scan Co4ECanvas o bang cu (dong 90: 'can chia thanh co4e_canvas_widget.py ... va canvas_interaction_mixin.py'). |
|
||||
| Co4ECanvas.dragEnterEvent | 671-675 | presentation/co4e/canvas_interaction_mixin.py | (khong co ghi chu) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach nhom su kien chuot/phim/keo-tha/zoom/pan/overlay ra mixin rieng). Ban do nay tach canvas_interaction_mixin.py theo dung de xuat trong note cua chinh scan Co4ECanvas o bang cu (dong 90: 'can chia thanh co4e_canvas_widget.py ... va canvas_interaction_mixin.py'). |
|
||||
| Co4ECanvas.dragMoveEvent | 677-681 | presentation/co4e/canvas_interaction_mixin.py | (khong co ghi chu) | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach nhom su kien chuot/phim/keo-tha/zoom/pan/overlay ra mixin rieng). Ban do nay tach canvas_interaction_mixin.py theo dung de xuat trong note cua chinh scan Co4ECanvas o bang cu (dong 90: 'can chia thanh co4e_canvas_widget.py ... va canvas_interaction_mixin.py'). |
|
||||
| Co4ECanvas.dropEvent | 683-701 | presentation/co4e/canvas_interaction_mixin.py | GOP 2 luot quet trung ky hieu: agent 1 doc duoc 683-700 (than try chua dong, cat ngang lat o ranh gioi doc 701-701), agent 2 doc duoc 683-701 (chi dong 701 e.acceptProposedAction() nam trong lat duoc giao, dong 702 la dong trong cuoi file). Doi chieu truc tiep voi ui/co4e_canvas.py xac nhan than ham thuc su ket thuc o dong 701 (701 dong tong cong ca file) -> chon 683-701. Xu ly drop tu sidebar: kind=='workflow' -> add_workflow, nguoc lai -> add_palette_step; import cuc bo workflow_from_dict/step_from_dict tu core/co4e.py; goi self.add_workflow/self.add_palette_step (co4e_canvas_widget.py) -> cross-file. Khong cham dia/mang, chi json.loads tu bytes mime trong bo nho. | co | Bang cu gop chung vao presentation/co4e/co4e_canvas_widget.py (chua tach nhom su kien chuot/phim/keo-tha/zoom/pan/overlay ra mixin rieng). Ban do nay tach canvas_interaction_mixin.py theo dung de xuat trong note cua chinh scan Co4ECanvas o bang cu (dong 90: 'can chia thanh co4e_canvas_widget.py ... va canvas_interaction_mixin.py'). |
|
||||
|
||||
## Cho thay khac tai lieu — can nguoi quyet
|
||||
|
||||
Khong co symbol nao trong 96 dong quet mang note bat dau bang `khac tai lieu:` (da kiem bang script, dem duoc 0). Muc nay de trong theo dung yeu cau tu kiem; khong co gi can nguoi quyet tu tieu chi nay.
|
||||
|
||||
Tuy nhien co 2 nhom lech thuc te voi bang cu toan file, liet ke de nguoi soat bien:
|
||||
|
||||
1. **2 symbol thieu hoan toan trong bang cu**: `Co4ECanvas._ZOOM_MIN`, `Co4ECanvas._ZOOM_MAX` (dong 294) — bang cu nhay tu dong 292 sang 296, bo sot hang class-constant nay.
|
||||
2. **57 symbol co trong bang cu nhung target khac** — bang cu (1-file) gop tat ca vao `presentation/co4e/co4e_canvas_widget.py`; ban do nay tach thanh 3 file (`canvas_items.py` cho _NodeItem/_EdgeItem/_status_color, `canvas_interaction_mixin.py` cho nhom su kien chuot/phim/keo-tha/zoom/pan/overlay, phan con lai o lai `co4e_canvas_widget.py`). Day la tach chi tiet hon, phu hop voi chinh ghi chu cua scan Co4ECanvas trong bang cu (dong 90) da de xuat huong tach nay.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,253 @@
|
||||
# Bản đồ tách file — Chat View (khung Messages / composer)
|
||||
|
||||
Gộp 220 symbol thô từ 3 agent quét song song trên `ui/co4e_tab.py`, còn lại **218 dòng** sau khi gộp 2 nhóm trùng lặp thật do lát quét cắt ngang symbol (`Co4ETab._reload_sidebar` dòng 686, `Co4ETab._run_from` dòng 1399 — xem bảng bên dưới) và giải quyết 5 dòng có `target_file` ban đầu ghi `unsure`.
|
||||
|
||||
- File đích của lane chat-view: `presentation/co4e/co4e_chat_view.py` — **21 symbol**.
|
||||
- Phần còn lại của container: `ui/co4e_tab.py` — **197 symbol**.
|
||||
- Thuộc lane/file khác (ngoài phạm vi hai đích trên): **0 symbol** — dữ liệu thô của lần quét này chỉ dùng đúng 2 target_file thật (`ui/co4e_tab.py`, `presentation/co4e/co4e_chat_view.py`) cộng `unsure`.
|
||||
- Note bắt đầu bằng `khac tai lieu:` tìm thấy trong dữ liệu thô: **0**.
|
||||
|
||||
## Các nhóm đã gộp (lát quét cắt ngang 1 symbol thành 2 mảnh)
|
||||
|
||||
- Co4ETab._reload_sidebar (dong 686 va 701, 2 luot quet trung, gop thanh 686-719)
|
||||
- Co4ETab._run_from (dong 1399 va unknown_method_fragment_before_1401 dong 1401, 2 luot quet trung, gop thanh 1399-1403)
|
||||
|
||||
## Anomaly cố ý giữ tách riêng (không gộp)
|
||||
|
||||
- `Co4ETab.showEvent` xuất hiện **2 lần** trong dữ liệu thô ở hai dòng khác nhau (939-941 và 1772-1775) — đây KHÔNG phải lỗi quét trùng mà là **2 định nghĩa method cùng tên thật sự tồn tại trong class** (định nghĩa thứ 2 đè lên định nghĩa đầu lúc runtime, hành vi Python bình thường). Khớp với `kept_separate_anomaly_symbols` đã ghi nhận ở cả bảng cũ (`co4e-split-map.json`) và bản đồ run-control — giữ tách riêng thành 2 dòng ở bảng dưới.
|
||||
|
||||
## Phát hiện quan trọng nhất từ việc đối chiếu với bảng cũ (`docs/architecture/co4e-split-map.json`, 363 dòng)
|
||||
|
||||
1. **Đường dẫn `presentation/co4e/co4e_tab.py` trong bảng cũ tương đương `ui/co4e_tab.py` hôm nay.** Bảng cũ dùng `target_file="presentation/co4e/co4e_tab.py"` cho phần thân lớp `Co4ETab` còn lại, nhưng file đó ở repo hiện tại chỉ là factory `build_co4e_tab()` mỏng (~53 dòng, bọc nguyên `Co4ETab` cũ 1:1) — KHÔNG phải nơi lớp `Co4ETab` thật sự sống (lớp đó vẫn ở `ui/co4e_tab.py`). Phát hiện này đã được xác nhận trước đó ở bản đồ run-control; bảng này tự quy đổi mọi so sánh trước khi kết luận lệch.
|
||||
2. **Phạm vi `co4e_chat_view.py` bị thu hẹp mạnh so với bảng cũ.** Bảng cũ (50 dòng liên quan) coi 'chat view' là toàn bộ chuỗi: khung widget (header Messages, `chat_stack`, composer, `_ChatInput`, RoutingToggle) **LẪN** business logic điều phối lượt chat (`_chat_send`, `_apply_co4e_routing`, `_run_chat_turn` + 4 closure lồng bên trong, `_extract_agent_directive`, `_resolve_agent`, `_append_chat`/`_append_diff`/`_append_plan`, `_fmt_usage`/`_apply_usage`/`_refresh_usage_total`, `_toggle_messages`/`_ensure_flow_log`/`_active_log`/`chat_log`/`_plan_bubble`). Lần quét 3-agent hiện tại (lặp lại trong từng ghi chú riêng lẻ với cụm từ 'theo yêu cầu KHÔNG thuộc co4e_chat_view.py') thu hẹp `co4e_chat_view.py` chỉ còn là **khung widget thuần túy**: `_ChatInput` + `_directive_token` (ô nhập autocomplete) và phần dựng UI của `_build_chat` (header/composer/stack). Toàn bộ business logic điều phối chat, quản lý log per-flow, usage và routing ở lại `ui/co4e_tab.py`. Đây là khác biệt lớn nhất giữa hai bảng — ảnh hưởng tới hơn 20 dòng bên dưới (đánh dấu ở cột cuối).
|
||||
3. **`_PLAN_GLYPH`/`_fmt_plan`/`_qcolor` bị bảng cũ xếp sai lane (không phải chat_view.py, cũng không phải ui/co4e_tab.py).** Cả ba đều phục vụ chat (glyph/format cho bong bóng 'plan', màu cột trạng thái) nhưng bảng cũ xếp chung vào `presentation/co4e/co4e_run_control_widget.py` — sai giống hệt phát hiện đã ghi nhận ở bản đồ run-control cho chính 3 symbol này. Thêm 2 symbol mới chưa từng được đối chiếu trước đó (`_skill_names`, `_agent_names`, cũng bị bảng cũ xếp thẳng vào `co4e_chat_view.py`) — quyết định ở đây là giữ cả 5 symbol tại `ui/co4e_tab.py` vì chúng dùng ở nhiều nơi ngoài phạm vi composer chat hoặc thuộc business logic thuần.
|
||||
4. **Bảng cũ bỏ sót 7 symbol** không có dòng nào dù nằm trong phạm vi đã quét: `_ChatInput.submit` (dòng 143), `Co4ETab.status_message` (dòng 232), `Co4ETab._build_sidebar.<locals>._Col` (dòng 505-514), `Co4ETab._build_sidebar.<locals>._Col.__init__` (dòng 508-509), `Co4ETab._build_sidebar.<locals>._Col.addWidget` (dòng 511-513), `Co4ETab._agent_panel` (dòng 557), `Co4ETab._run_chat_turn.job._emit` (dòng 1894-1901).
|
||||
|
||||
Tổng số dòng có lệch với bảng cũ (khác target_file sau khi quy đổi đường dẫn): **120/218**.
|
||||
|
||||
## Bảng đầy đủ (sắp theo số dòng)
|
||||
|
||||
| symbol | dòng | file đích | lý do | có trong bảng cũ chưa | chỗ nào thấy bảng cũ sai |
|
||||
|---|---|---|---|---|---|
|
||||
| `_PLAN_GLYPH` | 47-48 | `ui/co4e_tab.py` | hằng số module dùng bởi _fmt_plan cho việc hiển thị plan trong chat log — thuộc business logic của Co4ETab, không phải khung widget chat | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 45-46) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `_fmt_plan` | 51-60 | `ui/co4e_tab.py` | dùng bởi _append_plan (business logic per-flow, ở lại Co4ETab theo mô tả target) | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 49-58) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `_skill_names` | 63-67 | `ui/co4e_tab.py` | đọc skills từ đĩa qua core.skills.list_skills/builtin_skills; dùng ở nhiều nơi trong file (dòng 164, 714, 1294, 1321, 1344) không chỉ trong chat composer nên không chuyển riêng vào co4e_chat_view.py | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_chat_view.py` (dong 61-65) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `_agent_names` | 70-73 | `ui/co4e_tab.py` | đọc agents từ đĩa qua core.co4e.list_custom_agents; dùng ở nhiều nơi (dòng 168 và ngoài phạm vi đọc), giữ ở Co4ETab như _skill_names | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_chat_view.py` (dong 68-71) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `_EqualTabBar` | 76-96 | `ui/co4e_tab.py` | không thấy chỗ nào khởi tạo _EqualTabBar trong toàn file (grep 'flow_bar =' cho thấy dùng QTabBar thường ở dòng 735) — có thể là code chết, cần người quyết có xoá hay giữ -- QUYET DINH: khong tim thay noi nao khoi tao _EqualTabBar trong toan file (flow_bar dung QTabBar thuong o dong 735) -- co the la code chet, nhung du con hay khong no la mot QTabBar tien ich cho sidebar icon-tabs, KHONG lien quan chat -- khop voi quyet dinh da chot cho cung symbol nay o lane run-control (ui/co4e_tab.py). | co | - |
|
||||
| `_EqualTabBar.tabSizeHint` | 84-92 | `ui/co4e_tab.py` | thuộc _EqualTabBar — xem note ở class, có vẻ không còn dùng -- QUYET DINH: thuoc _EqualTabBar -- xem ly do o class, khong lien quan chat. | co | - |
|
||||
| `_EqualTabBar.resizeEvent` | 94-96 | `ui/co4e_tab.py` | thuộc _EqualTabBar — xem note ở class -- QUYET DINH: thuoc _EqualTabBar -- xem ly do o class, khong lien quan chat. | co | - |
|
||||
| `_PaletteList` | 99-121 | `ui/co4e_tab.py` | dùng cho wf_list ở sidebar (drag workflow lên canvas), không liên quan chat view | co | BANG CU KHAC: bang cu xep vao `unsure` (dong 97-119) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `_PaletteList.__init__` | 104-108 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `unsure` (dong 102-106) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `_PaletteList.startDrag` | 110-121 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `unsure` (dong 108-119) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `_directive_token` | 124-136 | `presentation/co4e/co4e_chat_view.py` | chỉ được gọi bởi _ChatInput (dòng 156, 194) — hàm thuần Python phục vụ autocomplete của ô nhập chat, nên đi cùng _ChatInput | co | - |
|
||||
| `_ChatInput` | 139-228 | `presentation/co4e/co4e_chat_view.py` | ô nhập của composer — nằm trong phạm vi widget khung chat theo mô tả target | co | - |
|
||||
| `_ChatInput.submit` | 143 | `presentation/co4e/co4e_chat_view.py` | Signal lớp, phát khi Enter được nhấn — Co4ETab._chat_send (nằm ngoài phạm vi đọc) sẽ nối vào signal này từ bên ngoài widget | chua | BANG CU BO SOT: khong co dong nao cho symbol nay trong bang cu (363 dong) du no ro rang ton tai trong pham vi da quet -- co the do agent quet truoc bo lot, hoac day la ten sinh ra tu buoc gop lat cua ban do nay (vd ten method suy doan/tam). |
|
||||
| `_ChatInput.__init__` | 145-153 | `presentation/co4e/co4e_chat_view.py` | - | co | - |
|
||||
| `_ChatInput._maybe_popup` | 155-180 | `presentation/co4e/co4e_chat_view.py` | gọi _skill_names()/_agent_names() (đọc đĩa) để dựng popup gợi ý — phần Qt (định vị popup, resize) đòi QWidget sống | co | - |
|
||||
| `_ChatInput._add_row` | 182-186 | `presentation/co4e/co4e_chat_view.py` | - | co | - |
|
||||
| `_ChatInput._accept` | 188-201 | `presentation/co4e/co4e_chat_view.py` | - | co | - |
|
||||
| `_ChatInput.focusOutEvent` | 203-206 | `presentation/co4e/co4e_chat_view.py` | - | co | - |
|
||||
| `_ChatInput.keyPressEvent` | 208-228 | `presentation/co4e/co4e_chat_view.py` | phát submit khi Enter và popup không hiện — Co4ETab nối submit -> _chat_send ở ngoài widget | co | - |
|
||||
| `Co4ETab` | 231-700 | `ui/co4e_tab.py` | cắt ngang lát — cần agent gộp đối chiếu (class tiếp tục sau dòng 700) | co | - |
|
||||
| `Co4ETab.status_message` | 232 | `ui/co4e_tab.py` | Signal lớp | chua | BANG CU BO SOT: khong co dong nao cho symbol nay trong bang cu (363 dong) du no ro rang ton tai trong pham vi da quet -- co the do agent quet truoc bo lot, hoac day la ten sinh ra tu buoc gop lat cua ban do nay (vd ten method suy doan/tam). |
|
||||
| `Co4ETab.__init__` | 234-306 | `ui/co4e_tab.py` | dựng toàn bộ layout 3 cột (sidebar/center/config); chưa thấy lệnh dựng chat panel trong phạm vi 1-700 — có thể nằm trong _build_center() ở dòng > 700 | co | - |
|
||||
| `Co4ETab.ctx` | 236 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab._wf` | 237 | `ui/co4e_tab.py` | trạng thái chia sẻ rộng — flow đang hiển thị trên canvas; nhiều method (kể cả chat log lookup ngoài phạm vi đọc) phụ thuộc vào self._wf | co | - |
|
||||
| `Co4ETab._chat_worker` | 238 | `ui/co4e_tab.py` | AgentWorker của chat — theo mô tả target, _chat_send/job()/AgentWorker KHÔNG chuyển vào co4e_chat_view.py, ở lại Co4ETab | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_chat_view.py` (dong 236) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab.manager` | 240 | `ui/co4e_tab.py` | Co4ERunManager — đã có RunsPagePanel/co4e_run_control_widget.py riêng, nhưng self.manager là thuộc tính của Co4ETab, ở lại | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 238) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._flow_runs` | 245 | `ui/co4e_tab.py` | trạng thái chia sẻ: wf_id -> run_id đang chạy trên canvas, dùng bởi nhiều method flow tab | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 243) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._run_logs` | 246 | `ui/co4e_tab.py` | map run_id -> ChatView; theo mô tả target đây là business logic per-flow, ở lại Co4ETab dù ChatView instance được hiển thị trong QStackedWidget của co4e_chat_view.py | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 244) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._flow_outputs` | 247 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `application/workflows/co4e_workflow_service.py` (dong 245) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._flow_usage` | 250 | `ui/co4e_tab.py` | usage per-flow (↓in ↑out ▤ctx $cost) — hiển thị ở label usage-total trong composer của co4e_chat_view.py, nhưng dữ liệu và logic tính toán ở lại Co4ETab (chỉ phần dựng label rỗng thuộc chat_view) | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 248) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._project_id` | 251 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `unsure` (dong 249) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._project_dir` | 252 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `unsure` (dong 250) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._manual_active` | 254 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 252) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._manual_order` | 255 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 253) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._manual_idx` | 256 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 254) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._flows` | 259 | `ui/co4e_tab.py` | trạng thái chia sẻ — danh sách flow đang mở dạng tab kiểu trình duyệt, dùng bởi hầu hết method _*flow_tab* | co | - |
|
||||
| `Co4ETab._active_flow_idx` | 260 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab._split` | 263 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab.config` | 270 | `ui/co4e_tab.py` | StepConfigPanel — đã tách sang presentation/co4e/node_property_panel.py ở làn khác; thuộc tính self.config trên Co4ETab ở lại đây | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/node_property_panel.py` (dong 268) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._config_collapsed` | 276 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab._config_expanded_w` | 277 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab._narrow_guard` | 282 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab._open_flow` | 309-340 | `ui/co4e_tab.py` | đọc/ghi self._flows, self.flow_bar — trạng thái chia sẻ giữa các flow tab | co | - |
|
||||
| `Co4ETab._close_other_flows` | 342-357 | `ui/co4e_tab.py` | self._flows, self._active_flow_idx | co | - |
|
||||
| `Co4ETab._show_runs` | 359-369 | `ui/co4e_tab.py` | self.flow_bar dùng chung với logic mở flow tab | co | - |
|
||||
| `Co4ETab._on_flow_tab_changed` | 371-387 | `ui/co4e_tab.py` | đọc self.center_stack (được gán ở ngoài phạm vi đọc, có thể trong _build_center) — trạng thái chia sẻ quan trọng theo mô tả của team | co | - |
|
||||
| `Co4ETab._sync_runs_toggle` | 389-396 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab._add_tab_close_button` | 398-408 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab._close_flow_tab_button` | 410-414 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab._close_flow_tab` | 416-444 | `ui/co4e_tab.py` | self._flows, self._flow_runs, self._run_logs — trạng thái chia sẻ giữa flow tab và run log của chat | co | - |
|
||||
| `Co4ETab._sync_active_flow_tab_text` | 446-449 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab._reflect_active_run` | 451-459 | `ui/co4e_tab.py` | self._flow_runs, self.canvas — canvas là thuộc tính được gán ngoài phạm vi đọc | co | BANG CU KHAC: bang cu xep vao `unsure` (dong 449-457) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._cur_run_id` | 462-475 | `ui/co4e_tab.py` | self._wf, self._flow_runs — logic thuần, không đụng Qt trực tiếp (chỉ đọc self._wf là attribute) | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 460-473) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._outputs_for` | 477-480 | `ui/co4e_tab.py` | self._flow_outputs — logic thuần | co | BANG CU KHAC: bang cu xep vao `application/workflows/co4e_workflow_service.py` (dong 475-478) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._update_run_btn` | 482-484 | `ui/co4e_tab.py` | self.run_btn được gán ngoài phạm vi đọc | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 480-482) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._build_sidebar` | 487-600 | `ui/co4e_tab.py` | method dài 113 dòng, gộp nhiều việc không liên quan: dựng section Workflows (list+CRUD+run-bg), section Agents (AgentListPanel), section Skills (SkillsListPanel), và section Runs (danh sách rút gọn) — nên tách thành các hàm _build_workflows_section/_build_agents_section/_build_skills_section/_build_runs_section riêng | co | - |
|
||||
| `Co4ETab._sections` | 495 | `ui/co4e_tab.py` | trạng thái chia sẻ giữa _section/_fold_section/_sync_section_arrow — map key -> (header, body, stretch) | co | - |
|
||||
| `Co4ETab.sidebar` | 496 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab.side_split` | 500 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab._build_sidebar.<locals>._Col` | 505-514 | `ui/co4e_tab.py` | lớp adapter cục bộ bên trong _build_sidebar, bọc QSplitter để các section builder gọi .addWidget(w, stretch) như trước | chua | BANG CU BO SOT: khong co dong nao cho symbol nay trong bang cu (363 dong) du no ro rang ton tai trong pham vi da quet -- co the do agent quet truoc bo lot, hoac day la ten sinh ra tu buoc gop lat cua ban do nay (vd ten method suy doan/tam). |
|
||||
| `Co4ETab._build_sidebar.<locals>._Col.__init__` | 508-509 | `ui/co4e_tab.py` | - | chua | BANG CU BO SOT: khong co dong nao cho symbol nay trong bang cu (363 dong) du no ro rang ton tai trong pham vi da quet -- co the do agent quet truoc bo lot, hoac day la ten sinh ra tu buoc gop lat cua ban do nay (vd ten method suy doan/tam). |
|
||||
| `Co4ETab._build_sidebar.<locals>._Col.addWidget` | 511-513 | `ui/co4e_tab.py` | - | chua | BANG CU BO SOT: khong co dong nao cho symbol nay trong bang cu (363 dong) du no ro rang ton tai trong pham vi da quet -- co the do agent quet truoc bo lot, hoac day la ten sinh ra tu buoc gop lat cua ban do nay (vd ten method suy doan/tam). |
|
||||
| `Co4ETab.wf_new_btn` | 518 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab.wf_list` | 529 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab.wf_edit_btn` | 536 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab.wf_dup_btn` | 537 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab.wf_del_btn` | 538 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab.wf_runbg_btn` | 545 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab._agent_panel` | 557 | `ui/co4e_tab.py` | AgentListPanel — widget đã tách sẵn ở làn khác (presentation/co4e/agent_list_panel.py), Co4ETab chỉ giữ tham chiếu và nối signal | chua | BANG CU BO SOT: khong co dong nao cho symbol nay trong bang cu (363 dong) du no ro rang ton tai trong pham vi da quet -- co the do agent quet truoc bo lot, hoac day la ten sinh ra tu buoc gop lat cua ban do nay (vd ten method suy doan/tam). |
|
||||
| `Co4ETab.ag_new_btn` | 558 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/agent_list_panel.py` (dong 550) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab.agent_list` | 560 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/agent_list_panel.py` (dong 558) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab.ag_edit_btn` | 561 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/agent_list_panel.py` (dong 562) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab.ag_del_btn` | 563 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/agent_list_panel.py` (dong 563) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._skills_panel` | 572 | `ui/co4e_tab.py` | SkillsListPanel — đã tách sẵn ở làn khác (presentation/co4e/skills_list_panel.py) | co | - |
|
||||
| `Co4ETab.sk_manage_btn` | 573 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab.skill_list` | 575 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab.runs_more_btn` | 583 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 586) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab.runs_side_list` | 591 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 594) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._SIDE_RUNS` | 602 | `ui/co4e_tab.py` | hằng số lớp | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 605) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._refresh_side_runs` | 604-616 | `ui/co4e_tab.py` | self.manager.runs() — trạng thái run manager | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 607-619) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._on_side_run_clicked` | 618-626 | `ui/co4e_tab.py` | self.runs_table được gán ngoài phạm vi đọc | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 621-629) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._section` | 628-660 | `ui/co4e_tab.py` | ghi vào self._sections — trạng thái chia sẻ | co | - |
|
||||
| `Co4ETab._fold_section` | 662-674 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab._sync_section_arrow` | 676-678 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab._icon_btn` | 680-684 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab._reload_sidebar` | 686-719 | `ui/co4e_tab.py` | [GOP 2 luot quet trung ky hieu, dong 686] mot agent doc than den 700 (cat ngang lat), agent kia doc tiep 701-719 (vong lap nap agent list + skill list vao palette qua co4e.list_custom_agents()/skills_mod.skill_prefix_for) -- QUYET DINH: nap lai toan bo sidebar (Workflows/Agents/Skills/Runs quick-list), khong lien quan chat -- khop voi quyet dinh da chot o lane run-control (dong 685-718 trong ban do do). | co | - |
|
||||
| `Co4ETab._palette_item` | 721-725 | `ui/co4e_tab.py` | helper tĩnh dựng QListWidgetItem với icon() — không liên quan chat | co | BANG CU KHAC: bang cu xep vao `unsure` (dong 724-728) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._build_center` | 728-868 | `ui/co4e_tab.py` | method dài ~140 dòng, gộp nhiều việc không liên quan: (1) dựng flow tab bar + scroll ẩn (không hiển thị), (2) center_stack + trang Runs, (3) toolbar flow editor (name_edit/add/save/mode/run/runs_btn), (4) canvas + overlay zoom, (5) tích hợp splitter canvas/chat qua self._build_chat(). Nên tách nhỏ thêm. Đọc/ghi self.center_stack, self.canvas, self._vsplit — trạng thái chia sẻ rộng với nhiều nhóm chức năng khác (canvas widget, run control, chat view) | co | - |
|
||||
| `Co4ETab.flow_bar` | 735 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab.flow_add_btn` | 761 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab.flow_scroll` | 778 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab.center_stack` | 802 | `ui/co4e_tab.py` | trạng thái chia sẻ — self.center_stack được dùng bởi _build_runs_page, _show_runs và nhiều nơi khác ngoài lát này; chuyển trang giữa Runs table và flow editor | co | - |
|
||||
| `Co4ETab.name_edit` | 811 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab.add_step_btn` | 816 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab.save_btn` | 819 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab.save_tpl_btn` | 823-824 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab.mode_combo` | 825 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 828) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab.run_btn` | 830 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 833) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab.runs_btn` | 837 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 840) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab.canvas` | 853 | `ui/co4e_tab.py` | trạng thái chia sẻ rộng — self.canvas đọc/ghi bởi rất nhiều method trong và ngoài lát này (add_blank_step, _on_node_selected, _on_config_changed, _sync_wf_from_canvas, _apply_workflow, _start_canvas_run...) | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_canvas_widget.py` (dong 856) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._vsplit` | 860 | `ui/co4e_tab.py` | trạng thái chia sẻ — self._vsplit dùng bởi _toggle_messages để co giãn giữa canvas và chat box; đúng như cảnh báo trong đề bài | co | - |
|
||||
| `Co4ETab._build_runs_page` | 870-898 | `ui/co4e_tab.py` | chỉ wiring RunsPagePanel (đã tách ở co4e_run_control_widget.py, không thuộc lát này) vào handler của Co4ETab — ở lại ui/co4e_tab.py theo đúng mô tả docstring của method | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 873-932) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab.runs_back_btn` | 881 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 882) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab.runs_title` | 883 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 887) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab.ws_folder_btn` | 884 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 892) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab.run_stop_btn` | 887 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 900) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab.run_rename_btn` | 889 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 905) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab.run_del_btn` | 891 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 909) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab.run_clear_btn` | 893 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 913) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab.runs_table` | 895 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 921) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._wrap_config` | 900-930 | `ui/co4e_tab.py` | không liên quan chat; liên quan node-property config panel (đã tách riêng ở node_property_panel.py, không thuộc lát/target được giao cho agent này) | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/node_property_panel.py` (dong 934-964) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab.config_toggle_btn` | 912 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/node_property_panel.py` (dong 946) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab.config_title` | 917 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/node_property_panel.py` (dong 951) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._cfg_vlayout` | 923 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/node_property_panel.py` (dong 957) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._cfg_top_spacer` | 927 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/node_property_panel.py` (dong 961) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._cfg_bot_spacer` | 928 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/node_property_panel.py` (dong 962) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab.config_container` | 929 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/node_property_panel.py` (dong 963) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._NARROW` | 937 | `ui/co4e_tab.py` | hằng số class-level (không phải self.<tên>) — ngưỡng bề rộng cửa sổ hẹp, không liên quan chat | co | - |
|
||||
| `Co4ETab.showEvent` | 939-941 | `ui/co4e_tab.py` | Qt override, không liên quan chat | co | - |
|
||||
| `Co4ETab._apply_narrow_layout` | 943-953 | `ui/co4e_tab.py` | không liên quan chat | co | - |
|
||||
| `Co4ETab._toggle_config` | 955-998 | `ui/co4e_tab.py` | không liên quan chat; điều khiển config panel + splitter self._split | co | - |
|
||||
| `Co4ETab._refresh_min_width` | 1000-1005 | `ui/co4e_tab.py` | không liên quan chat | co | - |
|
||||
| `Co4ETab._build_canvas_overlay` | 1007-1028 | `ui/co4e_tab.py` | overlay zoom cho canvas — không liên quan chat, thuộc nhóm canvas widget | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_canvas_widget.py` (dong 1041-1062) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab.zoom_in_btn` | 1020 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_canvas_widget.py` (dong 1054) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab.zoom_out_btn` | 1021 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_canvas_widget.py` (dong 1055) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab.fit_btn` | 1022 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_canvas_widget.py` (dong 1056) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._build_chat` | 1030-1093 | `presentation/co4e/co4e_chat_view.py` | Khớp trực tiếp với mô tả target: dựng header 'Messages' (icon+tiêu đề+nút thu/mở), QStackedWidget chat_stack chứa ChatView theo flow, composer (usage_total label + _ChatInput + RoutingToggle + nút Gửi). NHƯNG method này CŨNG khởi tạo trạng thái chia sẻ không thuộc widget thuần: self._flow_logs (dict per-flow), self._vsplit_sizes, self._msgs_collapsed (dùng bởi _toggle_messages/_ensure_flow_log ở lại Co4ETab) — nên tách phần init state đó ra khỏi hàm dựng widget khi chuyển file. Cũng nối self.chat_input.submit và self.chat_send_btn.clicked trực tiếp tới self._chat_send (method ở lại Co4ETab) — cần thiết kế callback/signal khi tách. | co | - |
|
||||
| `Co4ETab._chat_widget` | 1032 | `presentation/co4e/co4e_chat_view.py` | trạng thái chia sẻ — self._chat_widget.setMaximumHeight() được gọi từ _toggle_messages (ở lại Co4ETab) — điểm nối giữa 2 file | co | - |
|
||||
| `Co4ETab._mhdr` | 1038 | `presentation/co4e/co4e_chat_view.py` | trạng thái chia sẻ — self._mhdr.sizeHint() đọc từ _toggle_messages (ở lại Co4ETab) | co | - |
|
||||
| `Co4ETab.msgs_icon` | 1040 | `presentation/co4e/co4e_chat_view.py` | - | co | - |
|
||||
| `Co4ETab.msgs_title` | 1041 | `presentation/co4e/co4e_chat_view.py` | - | co | - |
|
||||
| `Co4ETab.chat_toggle_btn` | 1042 | `presentation/co4e/co4e_chat_view.py` | trạng thái chia sẻ — icon/tooltip của nút này bị _toggle_messages (ở lại Co4ETab) đổi qua lại icon 'chevron-up'/'chevron-down' | co | - |
|
||||
| `Co4ETab.chat_stack` | 1057 | `presentation/co4e/co4e_chat_view.py` | trạng thái chia sẻ quan trọng — self.chat_stack được _ensure_flow_log (addWidget) và _apply_workflow (setCurrentWidget) đọc/ghi, cả hai ở lại Co4ETab; điểm nối chính giữa chat_view.py và Co4ETab | co | - |
|
||||
| `Co4ETab._flow_logs` | 1058 | `ui/co4e_tab.py` | trạng thái chia sẻ per-flow (dict wf_id -> ChatView) — theo mô tả target, business logic quản lý log stays ở Co4ETab (_ensure_flow_log/_active_log/chat_log), nên dict này nên ở lại ui/co4e_tab.py dù được khởi tạo trong _build_chat (widget-building method) — cần tách khởi tạo này ra khỏi _build_chat khi chuyển file | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_chat_view.py` (dong 1092) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab.chat_input_row` | 1060 | `presentation/co4e/co4e_chat_view.py` | trạng thái chia sẻ — show()/hide() gọi từ _toggle_messages (ở lại Co4ETab) | co | - |
|
||||
| `Co4ETab._usage_total_lbl` | 1065 | `presentation/co4e/co4e_chat_view.py` | label usage-total của composer — có khả năng được cập nhật bởi _refresh_usage_total (không thuộc lát này, khả năng ở Co4ETab) — kiểm tra lại khi gộp | co | - |
|
||||
| `Co4ETab.chat_input` | 1071 | `presentation/co4e/co4e_chat_view.py` | _ChatInput — submit signal nối tới self._chat_send (method ở lại Co4ETab, không có trong lát này) | co | - |
|
||||
| `Co4ETab.chat_send_btn` | 1074 | `presentation/co4e/co4e_chat_view.py` | clicked nối tới self._chat_send (ở lại Co4ETab) | co | - |
|
||||
| `Co4ETab.co4e_routing_toggle` | 1079 | `presentation/co4e/co4e_chat_view.py` | - | co | - |
|
||||
| `Co4ETab._co4e_routed_provider` | 1080 | `ui/co4e_tab.py` | biến trạng thái routing override cho lượt chat kế tiếp — là business state hơn là widget, được đọc/ghi ở _chat_send (không thuộc lát này); không rõ nó nên ở composer widget hay ở lại Co4ETab, cần người quyết -- QUYET DINH: bien trang thai routing override cho luot chat ke tiep la BUSINESS STATE, khong phai widget -- cung mot self._co4e_routed_provider duoc gan lai o dong 1819 (trong _apply_co4e_routing, o lai ui/co4e_tab.py theo yeu cau de bai); khoi tao lan dau nay (dong 1080, ben trong _build_chat) nen duoc TACH RA khoi ham dung widget khi chuyen file, giong cach xu ly _flow_logs (dong 1058) -- quyet dinh o day = ui/co4e_tab.py. | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_chat_view.py` (dong 1114; 1853) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._vsplit_sizes` | 1087 | `ui/co4e_tab.py` | trạng thái chia sẻ với _toggle_messages (ở lại Co4ETab theo mô tả target) — dùng self._vsplit | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_chat_view.py` (dong 1121) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._msgs_collapsed` | 1088 | `ui/co4e_tab.py` | trạng thái chia sẻ với _toggle_messages (ở lại Co4ETab theo mô tả target) | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_chat_view.py` (dong 1122) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._toggle_messages` | 1095-1127 | `ui/co4e_tab.py` | loại trừ khỏi chat_view.py theo mô tả đề bài — dùng self._vsplit (splitter canvas/chat) để co giãn không gian, đây là logic của Co4ETab không phải của widget chat | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_chat_view.py` (dong 1129-1161) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._ensure_flow_log` | 1130-1139 | `ui/co4e_tab.py` | loại trừ khỏi chat_view.py theo mô tả đề bài — quản lý state per-flow (self._flow_logs, self.chat_stack) | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_chat_view.py` (dong 1164-1173) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._active_log` | 1141-1143 | `ui/co4e_tab.py` | loại trừ khỏi chat_view.py theo mô tả đề bài | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_chat_view.py` (dong 1175-1177) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab.chat_log` | 1145-1149 | `ui/co4e_tab.py` | property, loại trừ khỏi chat_view.py theo mô tả đề bài | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_chat_view.py` (dong 1180-1183) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._plan_bubble` | 1151-1157 | `ui/co4e_tab.py` | property với getter (1151-1153) và setter (1155-1157), loại trừ khỏi chat_view.py theo mô tả đề bài | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_chat_view.py` (dong 1186-1187) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._apply_workflow` | 1160-1173 | `ui/co4e_tab.py` | đọc/ghi self._wf, self.chat_stack, self.canvas — trạng thái chia sẻ rộng, business logic đổi workflow đang hiển thị | co | - |
|
||||
| `Co4ETab._new_workflow` | 1175-1183 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab._selected_wf` | 1185-1191 | `ui/co4e_tab.py` | co4e.get_workflow(ident) đọc từ repository lưu trữ workflow (đĩa) | co | - |
|
||||
| `Co4ETab._load_selected_workflow` | 1193-1196 | `ui/co4e_tab.py` | qua _selected_wf() chạm đĩa | co | - |
|
||||
| `Co4ETab._edit_selected_workflow` | 1198-1203 | `ui/co4e_tab.py` | qua _selected_wf() chạm đĩa | co | - |
|
||||
| `Co4ETab._duplicate_selected_workflow` | 1205-1212 | `ui/co4e_tab.py` | co4e.duplicate_workflow ghi đĩa | co | BANG CU KHAC: bang cu xep vao `application/workflows/co4e_workflow_service.py` (dong 1239-1246) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._wf_context_menu` | 1214-1237 | `ui/co4e_tab.py` | dựng QMenu và dispatch tới các method khác (một số chạm đĩa) — bản thân method này không chạm đĩa trực tiếp | co | - |
|
||||
| `Co4ETab._rename_workflow` | 1239-1255 | `ui/co4e_tab.py` | QInputDialog modal + co4e.save_workflow ghi đĩa; cũng đồng bộ self._wf.name nếu đang mở đúng flow — trạng thái chia sẻ với name_edit/self._wf | co | BANG CU KHAC: bang cu xep vao `application/workflows/co4e_workflow_service.py` (dong 1273-1289) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._delete_selected_workflow` | 1257-1263 | `ui/co4e_tab.py` | co4e.delete_workflow ghi đĩa | co | BANG CU KHAC: bang cu xep vao `application/workflows/co4e_workflow_service.py` (dong 1291-1297) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._sync_wf_from_canvas` | 1265-1268 | `ui/co4e_tab.py` | đọc self.canvas.nodes()/edges() và self.name_edit — trạng thái chia sẻ với canvas | co | - |
|
||||
| `Co4ETab._save` | 1270-1275 | `ui/co4e_tab.py` | co4e.save_workflow ghi đĩa | co | BANG CU KHAC: bang cu xep vao `application/workflows/co4e_workflow_service.py` (dong 1304-1309) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._autosave` | 1277-1280 | `ui/co4e_tab.py` | co4e.save_workflow ghi đĩa (chỉ khi workflow đã tồn tại) | co | BANG CU KHAC: bang cu xep vao `application/workflows/co4e_workflow_service.py` (dong 1311-1314) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._on_name_changed` | 1282-1284 | `ui/co4e_tab.py` | gọi self._sync_active_flow_tab_text() (không thuộc lát này) — cập nhật self._wf.name | co | - |
|
||||
| `Co4ETab._add_blank_step` | 1286-1288 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab._on_node_selected` | 1291-1297 | `ui/co4e_tab.py` | đọc self.canvas.nodes() và self.config — thuộc nhóm node-property, không phải chat | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/node_property_panel.py` (dong 1325-1331) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._on_config_changed` | 1299-1302 | `ui/co4e_tab.py` | gọi self._autosave() (ghi đĩa gián tiếp) | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/node_property_panel.py` (dong 1333-1336) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._new_agent` | 1305-1306 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/agent_list_panel.py` (dong 1339-1340) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._edit_agent` | 1308-1316 | `ui/co4e_tab.py` | co4e.list_custom_agents() khả năng đọc đĩa | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/agent_list_panel.py` (dong 1342-1350) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._edit_agent_dialog` | 1318-1324 | `ui/co4e_tab.py` | mở Co4EAgentDialog modal, co4e.save_custom_agent ghi đĩa | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/agent_list_panel.py` (dong 1352-1358) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._delete_agent` | 1326-1333 | `ui/co4e_tab.py` | co4e.delete_custom_agent ghi đĩa | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/agent_list_panel.py` (dong 1360-1367) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._manage_skills` | 1335-1339 | `ui/co4e_tab.py` | mở SkillsDialog modal | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/skills_list_panel.py` (dong 1369-1373) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._skill_map` | 1342-1348 | `ui/co4e_tab.py` | logic thuần Python, đọc skills_mod.skill_prefix_for(name) — có khả năng đọc file skill từ đĩa | co | BANG CU KHAC: bang cu xep vao `application/workflows/co4e_workflow_service.py` (dong 1376-1382) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._current_mode` | 1350-1351 | `ui/co4e_tab.py` | đọc self.mode_combo.currentData() | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 1384-1385) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._on_mode_changed` | 1353-1359 | `ui/co4e_tab.py` | reset self._manual_active/_manual_order/_manual_idx — trạng thái run-mode chia sẻ | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 1387-1393) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._on_run_clicked` | 1361-1371 | `ui/co4e_tab.py` | gọi self.manager.stop/self._start_canvas_run — thuộc nhóm run control | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 1395-1400) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._start_canvas_run` | 1373-1390 | `ui/co4e_tab.py` | khởi động self.manager.start(...) — sẽ trigger chạy step/agent (network/AI provider); ghi self._flow_runs, self._run_logs[run_id] = self.chat_log — điểm nối giữa run-control và chat log (self.chat_log là property loại trừ khỏi chat_view.py) | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 1407-1424) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._run_single` | 1392-1397 | `ui/co4e_tab.py` | gọi _start_canvas_run | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 1426-1431) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._run_from` | 1399-1403 | `ui/co4e_tab.py` | [GOP 2 luot quet trung ky hieu, dong 1399] mot agent doc phan dau (1399-1400, chua thay than ham), agent kia doc phan duoi (1401-1403, tag tam 'unknown_method_fragment_before_1401') voi than ham la self._start_canvas_run(..., only=self._downstream(node_id), seed=dict(self._outputs_for(self._wf.id))) -- QUYET DINH: chay lai tu 1 node cu the tren canvas (logic thuc thi run), khong lien quan chat; giu o ui/co4e_tab.py cung nhom voi _run_single/_downstream. | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 1433-1437) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._downstream` | 1405-1416 | `ui/co4e_tab.py` | đọc self.canvas.edges() — thuần logic đồ thị, test được không cần Qt nếu canvas là fake | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 1439-1450) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._manual_run_or_advance` | 1419-1432 | `ui/co4e_tab.py` | đọc/ghi self._wf, self._manual_order, self._manual_idx, self._manual_active, self._outputs_for — trạng thái chia sẻ giữa manual-run và canvas; gọi self.canvas.reset_statuses() | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 1453-1466) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._manual_step` | 1434-1450 | `ui/co4e_tab.py` | ghi self._flow_runs, self._run_logs (định tuyến log theo từng flow — trạng thái chia sẻ then chốt); self.manager.start có thể chạm đĩa/spawn agent; self.run_btn.setText cần widget sống | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 1468-1484) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._topo_order` | 1452-1457 | `ui/co4e_tab.py` | - | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 1486-1491) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._on_manager_event` | 1460-1516 | `ui/co4e_tab.py` | method dài (~57 dòng) gộp nhiều việc: định tuyến sự kiện theo run_id/flow, cập nhật canvas status, ghi self._outputs_for/self._run_logs/self._flow_runs (trạng thái chia sẻ nhiều flow song song), gọi self._append_chat/_append_diff/_append_plan, hiện popup thông báo — nên cân nhắc tách theo loại event | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 1494-1550) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._notify_run_finished` | 1518-1539 | `ui/co4e_tab.py` | dựng QMessageBox không chặn — cần QApplication sống | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 1552-1573) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._run_popups` | 1526-1527 | `ui/co4e_tab.py` | khởi tạo lười (hasattr guard) bên trong _notify_run_finished — không thấy gán trong __init__ ở lát này, agent đọc __init__ nên đối chiếu | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 1560-1561) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._refresh_runs` | 1541-1582 | `ui/co4e_tab.py` | cập nhật self.runs_table, self.flow_bar, self._sections — chạm nhiều widget cùng lúc | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 1575-1616) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._stop_selected_run` | 1584-1590 | `ui/co4e_tab.py` | đọc self.runs_table.currentRow() | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 1618-1624) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._delete_selected_run` | 1592-1605 | `ui/co4e_tab.py` | ghi self._flow_runs, self._run_logs — trạng thái chia sẻ per-flow | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 1626-1639) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._runs_context_menu` | 1607-1621 | `ui/co4e_tab.py` | dựng QMenu tại vị trí chuột — cần widget sống | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 1641-1655) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab.set_project` | 1624-1640 | `ui/co4e_tab.py` | ghi self._project_id, self._project_dir (trạng thái workspace chia sẻ, ảnh hưởng _flow_output_root/_out_dir); load_project đọc dữ liệu project (đĩa); gọi self._refresh_ws_folder_btn nếu widget tồn tại | co | - |
|
||||
| `Co4ETab._flow_output_root` | 1642-1652 | `ui/co4e_tab.py` | đọc self._project_dir, self.ctx.config — logic thuần tính đường dẫn, không tự chạm đĩa (không mkdir/open) | co | - |
|
||||
| `Co4ETab._refresh_ws_folder_btn` | 1654-1659 | `ui/co4e_tab.py` | self.ws_folder_btn.setText/setToolTip | co | - |
|
||||
| `Co4ETab._open_workspace_folder` | 1661-1668 | `ui/co4e_tab.py` | mkdir + open_location (mở file explorer hệ điều hành, spawn process) | co | - |
|
||||
| `Co4ETab._open_run_output_folder` | 1670-1681 | `ui/co4e_tab.py` | kiểm tra path.exists()/mkdir + open_location | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 1704-1715) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._rename_selected_run` | 1683-1714 | `ui/co4e_tab.py` | dựng QInputDialog; ghi self._flows, self.flow_bar (tab text), self._wf, self.name_edit — trạng thái chia sẻ giữa danh sách flow và tab đang mở; co4e.save_workflow ghi đĩa | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 1717-1748) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._run_selected_in_background` | 1716-1726 | `ui/co4e_tab.py` | self.manager.start khởi chạy agent nền (ghi output ra đĩa) | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 1750-1760) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._wf_by_id` | 1728-1736 | `ui/co4e_tab.py` | co4e.get_workflow đọc workflow đã lưu từ đĩa; đọc self._wf | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 1762-1770) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._rerun_run_item` | 1738-1749 | `ui/co4e_tab.py` | item là QTableWidgetItem; manager.start khởi chạy agent nền | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 1772-1783) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._open_run_from_table` | 1751-1770 | `ui/co4e_tab.py` | gọi self._open_flow(wf), self.canvas.update_node_status — chạm canvas widget và self._wf | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 1785-1804) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab.showEvent` | 1772-1775 | `ui/co4e_tab.py` | override Qt event | co | - |
|
||||
| `Co4ETab._out_dir` | 1777-1783 | `ui/co4e_tab.py` | mkdir(parents=True); đọc self._wf.name | co | - |
|
||||
| `Co4ETab._chat_send` | 1786-1812 | `ui/co4e_tab.py` | theo yêu cầu, _chat_send KHÔNG thuộc co4e_chat_view.py dù đọc self.chat_input — ở lại Co4ETab; đọc/ghi self._chat_worker | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_chat_view.py` (dong 1820-1846) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._apply_co4e_routing` | 1814-1849 | `ui/co4e_tab.py` | ghi self._co4e_routed_provider (trạng thái đọc lại trong _run_chat_turn/job); nhánh manual gọi confirm_switch — mở dialog Qt (routing_toggle) nên cần widget sống ở nhánh đó | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_chat_view.py` (dong 1848-1883) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._co4e_routed_provider` | 1819 | `ui/co4e_tab.py` | gán lại mỗi lần gọi _apply_co4e_routing; được đọc bằng getattr(...,'None') ở _run_chat_turn/job — không chắc có init trong __init__ (nằm ngoài lát này) | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_chat_view.py` (dong 1114; 1853) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._extract_agent_directive` | 1851-1857 | `ui/co4e_tab.py` | logic regex thuần, dễ test độc lập | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_chat_view.py` (dong 1885-1891) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._resolve_agent` | 1859-1867 | `ui/co4e_tab.py` | duyệt BUILTIN_AGENTS + co4e.list_custom_agents() (custom agents có thể đọc đĩa nhưng bản thân hàm chỉ gọi list) | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_chat_view.py` (dong 1893-1901) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._run_chat_turn` | 1869-1940 | `ui/co4e_tab.py` | method dài (~72 dòng) gộp: build prompt, định nghĩa 4 closure lồng nhau (job/on_event/done/failed), khởi AgentWorker — nên tách; theo yêu cầu KHÔNG đưa job()/AgentWorker sang co4e_chat_view.py; đọc/ghi self.chat_log, self._wf, self._chat_worker (trạng thái chia sẻ) | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_chat_view.py` (dong 1903-1974) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._run_chat_turn.job` | 1883-1912 | `ui/co4e_tab.py` | closure lồng trong _run_chat_turn, chạy trong AgentWorker thread; gọi run_cowork → gọi provider AI qua mạng; theo yêu cầu ở lại Co4ETab cùng _chat_send | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_chat_view.py` (dong 1917-1946) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._run_chat_turn.job._emit` | 1894-1901 | `ui/co4e_tab.py` | helper lồng bên trong job(), chuyển tiếp event streaming sang worker.emit_event | chua | BANG CU BO SOT: khong co dong nao cho symbol nay trong bang cu (363 dong) du no ro rang ton tai trong pham vi da quet -- co the do agent quet truoc bo lot, hoac day la ten sinh ra tu buoc gop lat cua ban do nay (vd ten method suy doan/tam). |
|
||||
| `Co4ETab._run_chat_turn.on_event` | 1914-1920 | `ui/co4e_tab.py` | closure cập nhật assistant.set_markdown/log.scroll_to_bottom — cần widget sống; gọi self._append_plan | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_chat_view.py` (dong 1948-1954) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._run_chat_turn.done` | 1922-1928 | `ui/co4e_tab.py` | ghi self._chat_worker=None, gọi self._apply_usage — chạm self._flow_usage | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_chat_view.py` (dong 1956-1962) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._run_chat_turn.failed` | 1930-1933 | `ui/co4e_tab.py` | ghi self._chat_worker=None, gọi self._append_chat | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_chat_view.py` (dong 1964-1967) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._append_chat` | 1942-1957 | `ui/co4e_tab.py` | theo yêu cầu, _append_chat KHÔNG thuộc co4e_chat_view.py — dùng self.chat_log mặc định, business logic per-flow ở lại Co4ETab | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_chat_view.py` (dong 1976-1991) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._fmt_usage` | 1960-1967 | `ui/co4e_tab.py` | logic thuần định dạng chuỗi + tra bảng giá từ self.ctx.config.data — test được không cần Qt | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_chat_view.py` (dong 1994-2001) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._apply_usage` | 1969-1986 | `ui/co4e_tab.py` | ghi self._flow_usage (tổng usage theo từng flow — trạng thái chia sẻ với composer label usage-total ở co4e_chat_view.py); gọi bub.add_usage cần widget bubble sống | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_chat_view.py` (dong 2003-2020) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._refresh_usage_total` | 1988-2003 | `ui/co4e_tab.py` | đọc self._flow_usage, self._wf; ghi self._usage_total_lbl.setText — label này được dựng trong co4e_chat_view.py (composer) nên đây là điểm nối trạng thái chia sẻ giữa 2 file | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_chat_view.py` (dong 2022-2037) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._append_diff` | 2005-2009 | `ui/co4e_tab.py` | theo yêu cầu KHÔNG thuộc co4e_chat_view.py — ở lại Co4ETab | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_chat_view.py` (dong 2039-2043) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._append_plan` | 2011-2022 | `ui/co4e_tab.py` | theo yêu cầu KHÔNG thuộc co4e_chat_view.py — dùng log._co4e_plan_bubble (state gắn trên đối tượng ChatView, không phải self) | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_chat_view.py` (dong 2045-2056) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `Co4ETab._retranslate` | 2025-2045 | `ui/co4e_tab.py` | duyệt self._sections (trạng thái chia sẻ toàn tab) và setText hàng loạt widget nhiều khu vực khác nhau (runs, sidebar, header) — cắt ngang nhiều nhóm chức năng khác nhau nên khó tách gọn | co | - |
|
||||
| `_html_escape` | 2048-2049 | `ui/co4e_tab.py` | hàm module-level, tiện ích thuần chuỗi, không thuộc riêng chat_view | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_chat_view.py` (dong 2082-2083) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
| `_qcolor` | 2052-2054 | `ui/co4e_tab.py` | QColor dùng như kiểu giá trị, không cần QApplication sống | co | BANG CU KHAC: bang cu xep vao `presentation/co4e/co4e_run_control_widget.py` (dong 2086-2088) nhung theo mo ta dich cua lan quet nay -> quyet dinh o day = `ui/co4e_tab.py`. |
|
||||
|
||||
## Chỗ thấy khác tài liệu — cần người quyết
|
||||
|
||||
Không có symbol nào trong 220 symbol thô có note bắt đầu bằng `khac tai lieu:` — mục này để trống theo đúng yêu cầu tự kiểm (không có gì cần liệt kê).
|
||||
@@ -0,0 +1,123 @@
|
||||
{
|
||||
"scope": "StepConfigPanel (ui/co4e_config_panel.py, dong 1-528)",
|
||||
"source_file": "ui/co4e_config_panel.py",
|
||||
"symbols": [
|
||||
{
|
||||
"symbol": "_SECTION_ANIM_MS",
|
||||
"source_lines": "27",
|
||||
"target_file": "presentation/co4e/step_config_section.py",
|
||||
"note": "hang so animation cho _add_section"
|
||||
},
|
||||
{
|
||||
"symbol": "_SectionHeader",
|
||||
"source_lines": "30-52",
|
||||
"target_file": "presentation/co4e/step_config_section.py",
|
||||
"note": "QLabel clickable, khung UI dung chung, khong co hanh vi nghiep vu rieng"
|
||||
},
|
||||
{
|
||||
"symbol": "_add_section",
|
||||
"source_lines": "55-130",
|
||||
"target_file": "presentation/co4e/step_config_section.py",
|
||||
"note": "khung section gap/mo dung chung cho 4 nhom truong cua StepConfigPanel"
|
||||
},
|
||||
{
|
||||
"symbol": "StepConfigPanel (Signal + __init__)",
|
||||
"source_lines": "133-313",
|
||||
"target_file": "presentation/co4e/node_property_panel.py",
|
||||
"note": "4 Signal (changed/run_node/run_from/delete_node) + dung toan bo form"
|
||||
},
|
||||
{
|
||||
"symbol": "StepConfigPanel.load_step",
|
||||
"source_lines": "316-354",
|
||||
"target_file": "presentation/co4e/node_property_panel.py",
|
||||
"note": "nap Step vao form"
|
||||
},
|
||||
{
|
||||
"symbol": "StepConfigPanel.clear_step",
|
||||
"source_lines": "356-359",
|
||||
"target_file": "presentation/co4e/node_property_panel.py",
|
||||
"note": "xoa state, tat panel"
|
||||
},
|
||||
{
|
||||
"symbol": "StepConfigPanel._on_edit",
|
||||
"source_lines": "362-378",
|
||||
"target_file": "presentation/co4e/node_property_panel.py",
|
||||
"note": "ghi field UI nguoc vao Step"
|
||||
},
|
||||
{
|
||||
"symbol": "StepConfigPanel._available_agent_names",
|
||||
"source_lines": "380-390",
|
||||
"target_file": "presentation/co4e/node_property_actions_mixin.py",
|
||||
"note": "staticmethod, chuyen vao mixin _StepConfigActionsMixin"
|
||||
},
|
||||
{
|
||||
"symbol": "StepConfigPanel._add_subagent",
|
||||
"source_lines": "392-408",
|
||||
"target_file": "presentation/co4e/node_property_actions_mixin.py",
|
||||
"note": "chuyen vao mixin _StepConfigActionsMixin"
|
||||
},
|
||||
{
|
||||
"symbol": "StepConfigPanel._edit_subagent",
|
||||
"source_lines": "410-428",
|
||||
"target_file": "presentation/co4e/node_property_actions_mixin.py",
|
||||
"note": "chuyen vao mixin _StepConfigActionsMixin"
|
||||
},
|
||||
{
|
||||
"symbol": "StepConfigPanel._del_subagent",
|
||||
"source_lines": "430-437",
|
||||
"target_file": "presentation/co4e/node_property_actions_mixin.py",
|
||||
"note": "chuyen vao mixin _StepConfigActionsMixin"
|
||||
},
|
||||
{
|
||||
"symbol": "StepConfigPanel._add_attachment",
|
||||
"source_lines": "439-453",
|
||||
"target_file": "presentation/co4e/node_property_actions_mixin.py",
|
||||
"note": "chuyen vao mixin _StepConfigActionsMixin"
|
||||
},
|
||||
{
|
||||
"symbol": "StepConfigPanel._del_attachment",
|
||||
"source_lines": "455-462",
|
||||
"target_file": "presentation/co4e/node_property_actions_mixin.py",
|
||||
"note": "chuyen vao mixin _StepConfigActionsMixin"
|
||||
},
|
||||
{
|
||||
"symbol": "StepConfigPanel._ai_draft",
|
||||
"source_lines": "464-498",
|
||||
"target_file": "presentation/co4e/node_property_actions_mixin.py",
|
||||
"note": "chuyen vao mixin _StepConfigActionsMixin; dung AgentWorker that"
|
||||
},
|
||||
{
|
||||
"symbol": "StepConfigPanel._load_models",
|
||||
"source_lines": "500-528",
|
||||
"target_file": "presentation/co4e/node_property_actions_mixin.py",
|
||||
"note": "chuyen vao mixin _StepConfigActionsMixin; dung AgentWorker that"
|
||||
}
|
||||
],
|
||||
"old_file_after_split": {
|
||||
"file": "ui/co4e_config_panel.py",
|
||||
"content": "module docstring (sua, van tieng Anh) + import lai StepConfigPanel tu presentation/co4e/node_property_panel.py + __all__",
|
||||
"lines": 14
|
||||
},
|
||||
"new_files": [
|
||||
{"file": "presentation/co4e/step_config_section.py", "lines": 134},
|
||||
{"file": "presentation/co4e/node_property_actions_mixin.py", "lines": 202},
|
||||
{"file": "presentation/co4e/node_property_panel.py", "lines": 293}
|
||||
],
|
||||
"inheritance": {
|
||||
"class": "StepConfigPanel",
|
||||
"bases": ["_StepConfigActionsMixin", "QScrollArea"],
|
||||
"mandatory_order": false,
|
||||
"reason": "khong co method nao cua _StepConfigActionsMixin trung ten voi QScrollArea (khac Co4ECanvas voi paintEvent/mousePressEvent), nen thu tu ke thua khong anh huong hanh vi; giu mixin-truoc chi de nhat quan quy uoc"
|
||||
},
|
||||
"deliberate_unused_import_kept": {
|
||||
"name": "PROVIDER_LABELS",
|
||||
"source_line": 21,
|
||||
"target_file": "presentation/co4e/node_property_panel.py",
|
||||
"reason": "khong dung o dau trong ban goc (da xac minh bang grep); giu nguyen de dung pham vi chi doi cho, khong don dep import thua"
|
||||
},
|
||||
"test_patch_adaptation": {
|
||||
"file": "tests/characterization/test_node_property_panel.py",
|
||||
"what_changed": "case CASE_ADD_SUBAGENT_EMPTY_NAMES_USES_GETTEXT_QUIRK_OK doi tu StepConfigPanel.__dict__[\"_available_agent_names\"] (luu roi gan lai) sang del StepConfigPanel._available_agent_names (xoa override de roi ve lai method ke thua tu mixin)",
|
||||
"why": "sau khi _available_agent_names chuyen vao _StepConfigActionsMixin, no khong con nam truc tiep trong StepConfigPanel.__dict__ nen loi KeyError; day la thay doi CACH patch/restore trong test, KHONG doi assert/hanh vi nao duoc kiem tra"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
# Bản đồ tách `StepConfigPanel` (`ui/co4e_config_panel.py` → `presentation/co4e/`)
|
||||
|
||||
- **Phạm vi lượt này:** chỉ `StepConfigPanel` (nguyên bản dòng 1-528 của
|
||||
`ui/co4e_config_panel.py`). Không đụng file nào khác thuộc làn N1
|
||||
(`ui/co4e_tab.py`, `ui/co4e_canvas.py`, `docs/architecture/co4e-split-map.md`/`.json`).
|
||||
- **Lý do phải tách thêm, dù chỉ 1 class:** `StepConfigPanel` một mình đã 396
|
||||
dòng (133-528); cộng thêm module docstring + khối import của một file riêng
|
||||
sẽ vượt trần 400 dòng (CASAN Check 2). Giải pháp: cắt-dán (không viết lại
|
||||
logic) thành 3 file theo trách nhiệm.
|
||||
|
||||
## File đích
|
||||
|
||||
| symbol | dòng gốc | file đích | ghi chú |
|
||||
|---|---|---|---|
|
||||
| `_SECTION_ANIM_MS` | 27 | `presentation/co4e/step_config_section.py` | hằng số dùng bởi `_add_section` |
|
||||
| `_SectionHeader` | 30-52 | `presentation/co4e/step_config_section.py` | `QLabel` clickable, không có hành vi nghiệp vụ riêng |
|
||||
| `_add_section` | 55-130 | `presentation/co4e/step_config_section.py` | khung ▶/▼ dùng chung cho 4 nhóm trường của `StepConfigPanel`; không đọc/ghi state của panel |
|
||||
| `StepConfigPanel` (Signal + `__init__`) | 133-313 | `presentation/co4e/node_property_panel.py` | 4 Signal (`changed`/`run_node`/`run_from`/`delete_node`) + dựng toàn bộ form |
|
||||
| `StepConfigPanel.load_step` | 316-354 | `presentation/co4e/node_property_panel.py` | nạp `Step` vào form |
|
||||
| `StepConfigPanel.clear_step` | 356-359 | `presentation/co4e/node_property_panel.py` | xoá state, tắt panel |
|
||||
| `StepConfigPanel._on_edit` | 362-378 | `presentation/co4e/node_property_panel.py` | ghi field UI ngược vào `Step` |
|
||||
| `StepConfigPanel._available_agent_names` | 380-390 | `presentation/co4e/node_property_actions_mixin.py` (`_StepConfigActionsMixin`) | staticmethod, dùng bởi `_add_subagent`/`_edit_subagent` |
|
||||
| `StepConfigPanel._add_subagent` | 392-408 | `presentation/co4e/node_property_actions_mixin.py` | |
|
||||
| `StepConfigPanel._edit_subagent` | 410-428 | `presentation/co4e/node_property_actions_mixin.py` | |
|
||||
| `StepConfigPanel._del_subagent` | 430-437 | `presentation/co4e/node_property_actions_mixin.py` | |
|
||||
| `StepConfigPanel._add_attachment` | 439-453 | `presentation/co4e/node_property_actions_mixin.py` | |
|
||||
| `StepConfigPanel._del_attachment` | 455-462 | `presentation/co4e/node_property_actions_mixin.py` | |
|
||||
| `StepConfigPanel._ai_draft` | 464-498 | `presentation/co4e/node_property_actions_mixin.py` | dùng `AgentWorker` thật (không mock trong `__init__`) |
|
||||
| `StepConfigPanel._load_models` | 500-528 | `presentation/co4e/node_property_actions_mixin.py` | dùng `AgentWorker` thật |
|
||||
|
||||
## Cách ghép lại: mixin + đa kế thừa
|
||||
|
||||
`node_property_panel.py`:
|
||||
```python
|
||||
class StepConfigPanel(_StepConfigActionsMixin, QScrollArea):
|
||||
...
|
||||
```
|
||||
|
||||
`_StepConfigActionsMixin` là mixin THUẦN — không `__init__` riêng, chỉ đọc/ghi
|
||||
state có sẵn trên `self` do `StepConfigPanel.__init__` định nghĩa
|
||||
(`self._step`, `self._node_id`, `self.ctx`, `self.sub_list`, `self.attach_list`,
|
||||
`self.instructions_edit`, `self.gen_btn`, `self.model_combo`,
|
||||
`self.load_models_btn`).
|
||||
|
||||
Khác với bước `co4e_canvas_widget.py` (Co4ECanvas override nhiều method Qt như
|
||||
`paintEvent`/`mousePressEvent`, nên thứ tự mixin-trước-base là **bắt buộc** để
|
||||
MRO ưu tiên override của mixin): ở đây **không có method nào của
|
||||
`_StepConfigActionsMixin` trùng tên với `QScrollArea`**, nên thứ tự kế thừa
|
||||
không ảnh hưởng hành vi. Giữ thứ tự mixin-trước chỉ để nhất quán quy ước, không
|
||||
phải yêu cầu kỹ thuật bắt buộc.
|
||||
|
||||
## Import thừa cố ý giữ nguyên
|
||||
|
||||
`PROVIDER_LABELS` (nguyên bản dòng 21, `from ..config import PROVIDER_LABELS`)
|
||||
không được dùng ở đâu trong toàn bộ `ui/co4e_config_panel.py` gốc (đã xác minh
|
||||
bằng grep). Vẫn giữ nguyên import này trong `node_property_panel.py` (chỉ đổi
|
||||
số cấp `..` → `...`), không xoá, để đúng phạm vi "chỉ dời chỗ" của lượt tách
|
||||
này — xoá một import "thừa" là một quyết định dọn dẹp ngoài phạm vi được giao.
|
||||
|
||||
## Thay đổi comment/test ngoài phạm vi "chỉ dời chỗ" (ghi riêng, không lẫn vào phần move)
|
||||
|
||||
1. **Docstring module của 3 file mới** (`step_config_section.py`,
|
||||
`node_property_actions_mixin.py`, `node_property_panel.py`) — viết MỚI hoàn
|
||||
toàn bằng tiếng Việt theo quy ước CASAN cho file mới trong `presentation/`
|
||||
(mẫu `infrastructure/persistence/json/atomic_json_file.py`). Đây không phải
|
||||
sửa một comment cũ bị sai do dời chỗ — module docstring nguyên bản (dòng
|
||||
1-9 của `ui/co4e_config_panel.py`) mô tả cả file cũ (đã bị chia làm 3), nên
|
||||
mỗi file mới cần một docstring kiến trúc riêng thay vì copy y hệt bản gốc.
|
||||
2. **`ui/co4e_config_panel.py`** (file cũ) — docstring được viết lại (vẫn
|
||||
tiếng Anh, khớp quy ước "sửa file cũ tiếng Anh thì giữ tiếng Anh") để nói rõ
|
||||
`StepConfigPanel` đã dời đi đâu, thay vì mô tả hành vi như thể class còn
|
||||
định nghĩa tại chỗ — comment cũ sẽ SAI (nói rằng lớp "ở đây" trong khi
|
||||
không còn) nếu giữ nguyên.
|
||||
3. **`tests/characterization/test_node_property_panel.py`** (dòng ~296-307
|
||||
nguyên bản) — SỬA kỹ thuật patch/restore của case
|
||||
`CASE_ADD_SUBAGENT_EMPTY_NAMES_USES_GETTEXT_QUIRK_OK`, không đổi bất kỳ
|
||||
assert/hành vi nào. Bản gốc dùng
|
||||
`StepConfigPanel.__dict__["_available_agent_names"]` để lưu lại method gốc
|
||||
trước khi monkey-patch, rồi gán lại y hệt lúc restore. Sau khi
|
||||
`_available_agent_names` chuyển vào `_StepConfigActionsMixin` (mixin riêng),
|
||||
nó không còn nằm trong `StepConfigPanel.__dict__` (chỉ được kế thừa qua
|
||||
MRO) → `KeyError`. Thay bằng: gán đè trực tiếp lên `StepConfigPanel` (vẫn
|
||||
shadow đúng như cũ), và khi xong dùng `del StepConfigPanel._available_agent_names`
|
||||
để nó rơi trở lại đúng method kế thừa từ mixin — hành vi quan sát được của
|
||||
test (các assert `sub_agents`) giữ nguyên 100%, chỉ đổi CÁCH lưu/khôi phục
|
||||
attribute bị monkey-patch. Đây là hệ quả tất yếu của yêu cầu tách mixin
|
||||
trong lượt này (test được viết khi class còn nguyên khối), không phải sửa
|
||||
test để che một thay đổi hành vi.
|
||||
|
||||
## Xác minh
|
||||
|
||||
- `.venv/Scripts/python.exe tools/check_co4e.py` — chạy TRƯỚC và SAU khi sửa,
|
||||
output giống hệt nhau cả 2 lần (`KET QUA: Co4E sap xep lai, khong mat control nao`).
|
||||
- `.venv/Scripts/python.exe -m pytest tests/characterization/test_node_property_panel.py -q`
|
||||
— `1 passed` cả trước (đo trên code gốc, class còn ở `ui/co4e_config_panel.py`)
|
||||
lẫn sau khi tách.
|
||||
- `.venv/Scripts/python.exe -m pytest tests -q --tb=short -rf --continue-on-collection-errors`
|
||||
— `287 passed, 1 skipped` sau khi tách (không có test nào khác vỡ vì import
|
||||
`StepConfigPanel` từ `ui/co4e_config_panel.py`).
|
||||
- AST-scan `domain/`+`application/` cho import PySide6/PyQt: `KHONG CO`.
|
||||
- Số dòng file mới: `node_property_panel.py` 293, `node_property_actions_mixin.py`
|
||||
202, `step_config_section.py` 134 — cả 3 đều ≤ 400.
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,238 @@
|
||||
# Bản đồ tách file — Run Control (trang Runs / Flow Status)
|
||||
|
||||
Gộp 215 symbol thô từ 3 agent quét song song trên `ui/co4e_tab.py`, còn lại **214 dòng** sau khi gộp 1 trùng lặp thật (`Co4ETab._reload_sidebar` dòng 685, xem bên dưới).
|
||||
|
||||
- File đích của lane run-control: `presentation/co4e/co4e_run_control_widget.py` — **18 symbol**.
|
||||
- Phần còn lại của container: `ui/co4e_tab.py` — **173 symbol**.
|
||||
- Thuộc lane/file khác (ngoài phạm vi run-control, chỉ ghi lại để tham khảo): **23 symbol** (application/workflows/co4e_workflow_service.py, presentation/co4e/agent_list_panel.py, presentation/co4e/skills_list_panel.py, presentation/co4e/co4e_canvas_widget.py).
|
||||
- Note bắt đầu bằng `khac tai lieu:` tìm thấy trong dữ liệu thô: **0**.
|
||||
|
||||
## Phát hiện quan trọng nhất từ việc đối chiếu với bảng cũ
|
||||
|
||||
1. **Đường dẫn `co4e_tab.py` trong bảng cũ sai.** Bảng cũ (`docs/architecture/co4e-split-map.json`) dùng `target_file="presentation/co4e/co4e_tab.py"` cho phần thân lớp `Co4ETab` còn lại, nhưng file đó ở repo hiện tại chỉ là factory `build_co4e_tab()` dài 53 dòng (bọc nguyên `Co4ETab` cũ 1:1, xem docstring của nó) — KHÔNG phải nơi lớp `Co4ETab` thật sự sống. Lớp đó vẫn đang ở `ui/co4e_tab.py`. Bảng này đã tự quy đổi mọi so sánh trước khi kết luận lệch.
|
||||
2. **Phạm vi `co4e_run_control_widget.py` đã bị thu hẹp so với bảng cũ.** Bảng cũ coi 'run control' là toàn bộ chuỗi: bảng Runs + logic thực thi run (start/stop/manual-mode/mode toolbar/event routing/popup) + sidebar quick-list các run gần đây. Lần quét 3-agent hiện tại (theo mô tả đích lặp lại trong nhiều ghi chú riêng lẻ) chỉ còn coi `co4e_run_control_widget.py` là **trang Runs/Flow Status**: bảng `runs_table` + các nút hành động trên từng dòng (stop/rename/delete/clear/mở thư mục) + nút quay lại + tiêu đề. Logic thực thi run, toolbar mode/run, và sidebar quick-list quay lại ở lại `ui/co4e_tab.py`. Đây là khác biệt lớn nhất — ảnh hưởng tới 34 dòng bên dưới (đánh dấu ở cột cuối).
|
||||
3. **Bảng cũ không nhất quán giữa alias của Agent panel và Skills panel.** `ag_new_btn`/`agent_list`/`ag_edit_btn`/`ag_del_btn` (alias trỏ vào `AgentListPanel` đã tách) được bảng cũ xếp vào `presentation/co4e/agent_list_panel.py`, nhưng `sk_manage_btn`/`skill_list` (alias trỏ vào `SkillsListPanel`, cùng bản chất) lại bị xếp vào `co4e_tab.py`. Bảng này chọn xử lý đồng nhất — coi cả hai cặp alias đều thuộc file panel tương ứng.
|
||||
4. **Bảng cũ bỏ sót 2 symbol:** `Co4ETab.status_message` (Signal cấp lớp) và `Co4ETab._agent_panel` (instance `AgentListPanel` do container giữ) không có dòng nào trong bảng cũ dù nằm trong phạm vi đã quét.
|
||||
|
||||
## Bảng đầy đủ (sắp theo số dòng)
|
||||
|
||||
| symbol | dòng | file đích | lý do | có trong bảng cũ chưa | chỗ nào thấy bảng cũ sai |
|
||||
|---|---|---|---|---|---|
|
||||
| `_PLAN_GLYPH` | 46-47 | `ui/co4e_tab.py` | module-level dict constant, không phải self.<attr> -- QUYET DINH: glyph dùng bởi _fmt_plan cho bong bóng 'plan' trong CHAT, không phải bảng Runs; bảng cũ xếp nhầm vào co4e_run_control_widget.py | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 45-46) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `_fmt_plan` | 50-59 | `ui/co4e_tab.py` | helper format cho _append_plan (chat), không đụng runs_table; bảng cũ xếp nhầm vào co4e_run_control_widget.py | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 49-58) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `_skill_names` | 62-66 | `ui/co4e_tab.py` | gọi skills_mod.list_skills()/builtin_skills() — có thể chạm đĩa qua core.skills -- QUYET DINH: helper autocomplete cho _ChatInput — thuộc lane co4e_chat_view.py (chưa tồn tại), ngoài phạm vi lane run-control nên giữ nguyên vị trí hiện tại | co | - |
|
||||
| `_agent_names` | 69-72 | `ui/co4e_tab.py` | gọi co4e.list_custom_agents() — chạm đĩa qua core.co4e -- QUYET DINH: cùng lý do với _skill_names — autocomplete /agent trong chat | co | - |
|
||||
| `_EqualTabBar` | 75-95 | `ui/co4e_tab.py` | helper QTabBar cho sidebar icon-tabs — không liên quan Flow Status -- QUYET DINH: QTabBar tiện ích cho sidebar icon-tabs, không liên quan Runs | co | - |
|
||||
| `_EqualTabBar._GAP` | 81 | `ui/co4e_tab.py` | hằng số nội bộ của _EqualTabBar | co | - |
|
||||
| `_EqualTabBar.tabSizeHint` | 83-91 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `_EqualTabBar.resizeEvent` | 93-95 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `_PaletteList` | 98-120 | `ui/co4e_tab.py` | list kéo-thả vào canvas cho Workflows/Agents/Skills palette — không phải Runs -- QUYET DINH: list kéo-thả cho palette Workflows/Agents/Skills, không phải Runs | co | - |
|
||||
| `_PaletteList.__init__` | 103-107 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `_PaletteList._payload_role` | 105 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `_PaletteList.startDrag` | 109-120 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `_directive_token` | 123-135 | `ui/co4e_tab.py` | logic thuần regex cho autocomplete /skill /agent trong chat — test được không cần Qt -- QUYET DINH: regex thuần cho autocomplete /skill /agent — lane co4e_chat_view.py (chưa tồn tại), giữ nguyên | co | - |
|
||||
| `_ChatInput` | 138-227 | `ui/co4e_tab.py` | ô chat với popup autocomplete — không liên quan Flow Status -- QUYET DINH: ô nhập chat với popup autocomplete — lane co4e_chat_view.py (chưa tồn tại), giữ nguyên | co | - |
|
||||
| `_ChatInput.submit` | 142 | `ui/co4e_tab.py` | Signal khai báo ở cấp lớp -- QUYET DINH: Signal của _ChatInput — đi cùng class | chua | - |
|
||||
| `_ChatInput.__init__` | 144-152 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `_ChatInput._popup` | 146 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `_ChatInput._maybe_popup` | 154-179 | `ui/co4e_tab.py` | gọi _skill_names/_agent_names (chạm đĩa gián tiếp) và định vị popup bằng tọa độ màn hình | co | - |
|
||||
| `_ChatInput._add_row` | 181-185 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `_ChatInput._accept` | 187-200 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `_ChatInput.focusOutEvent` | 202-205 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `_ChatInput.keyPressEvent` | 207-227 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab` | 230-700 | `ui/co4e_tab.py` | cắt ngang lát — cần agent gộp đối chiếu (thân lớp trải dài quá dòng 700, đây chỉ là phần đầu) -- QUYET DINH: lớp container chính — phần còn lại sau khi các widget con (canvas/node-property/agent/skills/run-control/chat) đã tách | co | - |
|
||||
| `Co4ETab.status_message` | 231 | `ui/co4e_tab.py` | Signal khai báo ở cấp lớp -- QUYET DINH: Signal cấp lớp của chính Co4ETab; KHÔNG có trong bảng cũ (thiếu sót ở đó) | chua | BANG CU BO SOT: khong co dong nao cho symbol nay du no ro rang ton tai trong pham vi da quet (367 symbol tho cua bang cu) -- co the do agent quet truoc bo lot. |
|
||||
| `Co4ETab.__init__` | 233-305 | `ui/co4e_tab.py` | gộp nhiều việc: khởi state run-per-flow, dựng splitter 3 cột, wiring StepConfigPanel, narrow-guard, rồi mở flow đầu tiên — biên độ rủi ro cao khi tách vì đụng gần hết thuộc tính self chia sẻ toàn tab -- QUYET DINH: constructor container — sẽ đổi để dựng Co4ERunControlWidget thay vì tự vẽ bảng Runs | co | - |
|
||||
| `Co4ETab.ctx` | 235 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab._wf` | 236 | `ui/co4e_tab.py` | trạng thái chia sẻ toàn tab — workflow đang hiển thị trên canvas, đọc/ghi bởi rất nhiều method (flow tabs, run, sidebar, config) -- QUYET DINH: trạng thái trung tâm toàn tab, container giữ | co | - |
|
||||
| `Co4ETab._chat_worker` | 237 | `ui/co4e_tab.py` | AgentWorker của chat — lane co4e_chat_view.py chưa tồn tại, giữ nguyên | co | - |
|
||||
| `Co4ETab.manager` | 239 | `ui/co4e_tab.py` | Co4ERunManager dùng chung giữa canvas (mirror trạng thái node) và trang Runs (self.runs_table — định nghĩa ngoài dòng 700, có thể ở co4e_run_control_widget) — điểm nối quan trọng giữa hai lát -- QUYET DINH: Co4ERunManager dùng chung giữa canvas, chat VÀ bảng Runs — quyết định: container SỞ HỮU, co4e_run_control_widget.py nhận qua constructor/callback (dependency injection) thay vì tự tạo. Bảng cũ xếp thẳng vào co4e_run_control_widget.py dù chính ghi chú của nó gọi đây là 'điểm nối' — coi là quá vội | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 238) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._flow_runs` | 244 | `ui/co4e_tab.py` | trạng thái chia sẻ wf_id -> active run id, đọc/ghi bởi _open_flow, _close_flow_tab, _cur_run_id, _reflect_active_run -- QUYET DINH: dict wf_id->run id, ghi bởi _open_flow/_close_flow_tab/_start_canvas_run (đều ở co4e_tab.py) — container giữ, run-control đọc/ghi qua callback. Bảng cũ xếp vào co4e_run_control_widget.py — không khớp nơi ghi chính | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 243) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._run_logs` | 245 | `ui/co4e_tab.py` | map run_id->ChatView (thuộc lane chat), ghi bởi _manual_step/_start_canvas_run ở co4e_tab.py — container giữ | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 244) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._flow_outputs` | 246 | `application/workflows/co4e_workflow_service.py` | đã có quyết định của lane workflow-service (bảng cũ); ngoài phạm vi lane run-control. Hiện tại vật lý vẫn còn ở ui/co4e_tab.py dòng 246 chờ lane đó dọn | co | - |
|
||||
| `Co4ETab._flow_usage` | 249 | `ui/co4e_tab.py` | usage token/cost hiển thị ở header CHAT (Messages), không phải bảng Runs — bảng cũ xếp nhầm vào co4e_run_control_widget.py | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 248) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._project_id` | 250 | `ui/co4e_tab.py` | workspace hiện chọn — dùng chung bởi chat (_out_dir) và nút mở-thư-mục của Runs; container giữ, expose qua callback | co | - |
|
||||
| `Co4ETab._project_dir` | 251 | `ui/co4e_tab.py` | cùng lý do với _project_id | co | - |
|
||||
| `Co4ETab._manual_active` | 253 | `ui/co4e_tab.py` | trạng thái MODE THỦ CÔNG — thuộc nhóm run-execution, mô tả đích lane này loại trừ mode/run toolbar khỏi co4e_run_control_widget.py. Bảng cũ xếp vào co4e_run_control_widget.py — mâu thuẫn trực tiếp với phạm vi hiện tại | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 252) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._manual_order` | 254 | `ui/co4e_tab.py` | cùng nhóm với _manual_active — xem lý do ở đó | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 253) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._manual_idx` | 255 | `ui/co4e_tab.py` | cùng nhóm với _manual_active — xem lý do ở đó | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 254) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._flows` | 258 | `ui/co4e_tab.py` | trạng thái chia sẻ danh sách flow đang mở (browser-style tabs) — đọc/ghi bởi toàn bộ nhóm _open_flow/_close_flow_tab/_on_flow_tab_changed -- QUYET DINH: danh sách flow-tab kiểu browser, không phải Runs | co | - |
|
||||
| `Co4ETab._active_flow_idx` | 259 | `ui/co4e_tab.py` | cùng nhóm trạng thái chia sẻ với _flows -- QUYET DINH: cùng nhóm với _flows | co | - |
|
||||
| `Co4ETab._split` | 262 | `ui/co4e_tab.py` | splitter 3 cột của cả tab | co | - |
|
||||
| `Co4ETab.config` | 269 | `ui/co4e_tab.py` | container giữ tham chiếu StepConfigPanel (đã tách ở node_property_panel.py, file cấm sửa của lane khác) — cùng khuôn mẫu với _agent_panel/_skills_panel: instance do container tạo/giữ, nội dung panel ở file riêng. Bảng cũ xếp thẳng dòng này vào node_property_panel.py — không nhất quán với cách nó xử lý _agent_panel | co | - |
|
||||
| `Co4ETab._config_collapsed` | 275 | `ui/co4e_tab.py` | trạng thái thu/phóng của KHUNG bọc quanh panel (co4e_tab.py), không phải nội dung panel | co | - |
|
||||
| `Co4ETab._config_expanded_w` | 276 | `ui/co4e_tab.py` | cùng lý do với _config_collapsed | co | - |
|
||||
| `Co4ETab._narrow_guard` | 281 | `ui/co4e_tab.py` | guard bố cục hẹp của cả tab | co | - |
|
||||
| `Co4ETab._open_flow` | 308-339 | `ui/co4e_tab.py` | đọc/ghi self._flows, self._active_flow_idx, self.flow_bar — quản lý flow tab kiểu browser, không phải trang Runs -- QUYET DINH: quản lý flow-tab kiểu browser | co | - |
|
||||
| `Co4ETab._close_other_flows` | 341-356 | `ui/co4e_tab.py` | đọc/ghi self._flows, self._active_flow_idx, self.flow_bar | co | - |
|
||||
| `Co4ETab._show_runs` | 358-368 | `ui/co4e_tab.py` | chuyển center_stack giữa flow editor và trang Runs; gọi bởi self.runs_btn (định nghĩa ngoài dòng 700, thuộc toolbar) và runs_more_btn (sidebar) — không thao tác trực tiếp runs_table nên không chắc thuộc co4e_run_control_widget hay ở lại co4e_tab.py làm điều phối trang -- QUYET DINH: điều phối chuyển trang center_stack giữa flow editor và trang Runs — container sở hữu center_stack; sẽ gọi API show()/hide() hoặc setCurrentWidget trên Co4ERunControlWidget thay vì tự vẽ | co | - |
|
||||
| `Co4ETab._on_flow_tab_changed` | 370-386 | `ui/co4e_tab.py` | đọc/ghi self._active_flow_idx, self.center_stack (self.center_stack định nghĩa ngoài dòng 700) | co | - |
|
||||
| `Co4ETab._sync_runs_toggle` | 388-395 | `ui/co4e_tab.py` | dùng getattr(self, 'runs_btn', None) vì runs_btn (toolbar, ngoài dòng 700) có thể chưa tồn tại — đồng bộ trạng thái toggle của trang Runs -- QUYET DINH: đồng bộ nút toggle runs_btn ở toolbar (thuộc co4e_tab.py, KHÔNG phải trang Runs) | co | - |
|
||||
| `Co4ETab._add_tab_close_button` | 397-407 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab._close_flow_tab_button` | 409-413 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab._close_flow_tab` | 415-443 | `ui/co4e_tab.py` | đọc/ghi self._flow_runs, self._run_logs, self._flows, self._active_flow_idx, self.run_btn (định nghĩa ngoài dòng 700) — nhiều trạng thái chia sẻ chạm cùng lúc | co | - |
|
||||
| `Co4ETab._sync_active_flow_tab_text` | 445-448 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab._reflect_active_run` | 450-458 | `ui/co4e_tab.py` | đọc self.manager.all_runs(), ghi self._flow_runs, gọi self.canvas.update_node_status — cầu nối giữa run manager (chia sẻ với trang Runs) và canvas -- QUYET DINH: cầu nối manager -> canvas (update_node_status trên canvas thuộc co4e_tab.py) — đi cùng nhóm thực thi run, không phải bảng Runs | co | - |
|
||||
| `Co4ETab._cur_run_id` | 461-474 | `ui/co4e_tab.py` | logic thuần: đọc self._wf, self._flow_runs, self.manager.get() — test được không cần Qt -- QUYET DINH: tra cứu run đang chạy CỦA FLOW HIỆN TẠI, dùng bởi _reflect_active_run (canvas mirror, co4e_tab.py) — không đụng runs_table. Bảng cũ xếp vào co4e_run_control_widget.py — không khớp nơi dùng chính | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 460-473) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._outputs_for` | 476-479 | `application/workflows/co4e_workflow_service.py` | logic thuần dict, test được không cần Qt -- QUYET DINH: đã có quyết định của lane workflow-service (bảng cũ) cho cặp _flow_outputs/_outputs_for; ngoài phạm vi lane run-control | co | - |
|
||||
| `Co4ETab._update_run_btn` | 481-483 | `ui/co4e_tab.py` | thuộc nhóm run toolbar (self.run_btn) — theo mô tả target, co4e_run_control_widget KHÔNG bao gồm mode/run toolbar nên method này không nên vào đó -- QUYET DINH: nút Run của toolbar mode/run — mô tả đích loại trừ nhóm này khỏi co4e_run_control_widget.py; bảng cũ xếp vào đó — mâu thuẫn với phạm vi hiện tại | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 480-482) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._build_sidebar` | 486-599 | `ui/co4e_tab.py` | method dài >80 dòng, gộp nhiều việc không liên quan nhau: dựng section Workflows (list + edit/dup/del/run-bg), section Agents (AgentListPanel wiring), section Skills (SkillsListPanel wiring), section Runs sidebar quick-list, và lắp splitter dọc side_split — nên tách nhỏ thêm theo từng section -- QUYET DINH: dựng toàn bộ sidebar (Workflows/Agents/Skills/Runs quick-list) | co | - |
|
||||
| `Co4ETab._sections` | 494 | `ui/co4e_tab.py` | dict trạng thái chia sẻ cho các section sidebar (fold/unfold) — đọc/ghi bởi _section, _fold_section, _sync_section_arrow | co | - |
|
||||
| `Co4ETab.sidebar` | 495 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab.side_split` | 499 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `_build_sidebar._Col` | 504-512 | `ui/co4e_tab.py` | class cục bộ (locals) bên trong _build_sidebar — adapter cho side_split, không phải class module-level -- QUYET DINH: class cục bộ, adapter cho side_split | co | - |
|
||||
| `_Col.__init__` | 507-508 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `_Col.addWidget` | 510-512 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab.wf_new_btn` | 517 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab.wf_list` | 528 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab.wf_edit_btn` | 535 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab.wf_dup_btn` | 536 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab.wf_del_btn` | 537 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab.wf_runbg_btn` | 544 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab._agent_panel` | 556 | `ui/co4e_tab.py` | AgentListPanel — panel đã tách sẵn ở presentation/co4e/agent_list_panel.py (ngoài phạm vi lát này) -- QUYET DINH: container giữ instance AgentListPanel (đã tách sẵn) — KHÔNG có trong bảng cũ (thiếu sót ở đó, ag_new_btn/agent_list/ag_edit_btn/ag_del_btn có dòng nhưng _agent_panel thì không) | chua | BANG CU BO SOT: khong co dong nao cho symbol nay du no ro rang ton tai trong pham vi da quet (367 symbol tho cua bang cu) -- co the do agent quet truoc bo lot. |
|
||||
| `Co4ETab.ag_new_btn` | 557 | `presentation/co4e/agent_list_panel.py` | alias trỏ tới self._agent_panel.new_btn — widget thật nằm ở agent_list_panel.py | co | - |
|
||||
| `Co4ETab.agent_list` | 559 | `presentation/co4e/agent_list_panel.py` | alias trỏ tới self._agent_panel.list_widget | co | - |
|
||||
| `Co4ETab.ag_edit_btn` | 560 | `presentation/co4e/agent_list_panel.py` | alias trỏ tới self._agent_panel.edit_btn | co | - |
|
||||
| `Co4ETab.ag_del_btn` | 562 | `presentation/co4e/agent_list_panel.py` | alias trỏ tới self._agent_panel.del_btn | co | - |
|
||||
| `Co4ETab._skills_panel` | 571 | `ui/co4e_tab.py` | SkillsListPanel — panel đã tách sẵn ở presentation/co4e/skills_list_panel.py (ngoài phạm vi lát này) -- QUYET DINH: container giữ instance SkillsListPanel (đã tách sẵn) — khớp bảng cũ | co | - |
|
||||
| `Co4ETab.sk_manage_btn` | 572 | `presentation/co4e/skills_list_panel.py` | alias trỏ tới self._skills_panel.manage_btn — cùng bản chất với ag_new_btn ở trên. Bảng cũ xếp dòng này (và skill_list) vào co4e_tab.py trong khi ag_* tương ứng lại xếp vào agent_list_panel.py — KHÔNG NHẤT QUÁN giữa 2 cặp alias giống hệt nhau trong cùng bảng cũ; ở đây chọn xử lý đồng nhất với ag_* | co | BANG CU KHONG NHAT QUAN: xep Co4ETab.sk_manage_btn vao ui/co4e_tab.py trong khi alias tuong duong ben Agent (ag_new_btn/agent_list/...) lai xep vao agent_list_panel.py; o day chon dong nhat -> presentation/co4e/skills_list_panel.py. |
|
||||
| `Co4ETab.skill_list` | 574 | `presentation/co4e/skills_list_panel.py` | alias trỏ tới self._skills_panel.list_widget — xem ghi chú ở sk_manage_btn | co | BANG CU KHONG NHAT QUAN: xep Co4ETab.skill_list vao ui/co4e_tab.py trong khi alias tuong duong ben Agent (ag_new_btn/agent_list/...) lai xep vao agent_list_panel.py; o day chon dong nhat -> presentation/co4e/skills_list_panel.py. |
|
||||
| `Co4ETab.runs_more_btn` | 582 | `ui/co4e_tab.py` | nút icon trong sidebar mở trang Runs (gọi self._show_runs(True)) — điểm vào trang Flow Status nhưng không phải một phần của bảng runs_table -- QUYET DINH: nút icon nằm trong sidebar (dựng bởi _build_sidebar, ở co4e_tab.py) mở trang Runs — không phải một phần của runs_table. Bảng cũ xếp vào co4e_run_control_widget.py vì cùng 'chủ đề Runs', nhưng nơi nó được XÂY lại là sidebar | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 586) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab.runs_side_list` | 590 | `ui/co4e_tab.py` | danh sách rút gọn các run trong sidebar — KHÁC với self.runs_table của trang Flow Status (định nghĩa ngoài dòng 700, có thể ở co4e_run_control_widget); dễ nhầm là cùng một bảng nên tách agent gộp cần đối chiếu -- QUYET DINH: danh sách rút gọn run trong sidebar, KHÁC self.runs_table của trang Flow Status — dựng trong _build_sidebar (co4e_tab.py). Mô tả đích của _build_runs_page nói rõ co4e_run_control_widget.py = bảng runs_table + nút hành động, không bao gồm sidebar quick-list | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 594) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._SIDE_RUNS` | 601 | `ui/co4e_tab.py` | hằng số class — số run hiển thị trong sidebar quick-list, không phải trang runs_table chính -- QUYET DINH: hằng số cho sidebar quick-list — cùng nhóm runs_side_list | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 605) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._refresh_side_runs` | 603-615 | `ui/co4e_tab.py` | đọc self.manager.runs(), ghi self.runs_side_list — sidebar quick-list, khác trang Flow Status runs_table -- QUYET DINH: cùng nhóm runs_side_list | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 607-619) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._on_side_run_clicked` | 617-625 | `ui/co4e_tab.py` | gọi self._show_runs(True) rồi đọc self.runs_table (định nghĩa ngoài dòng 1-700, khả năng ở co4e_run_control_widget) để chọn dòng tương ứng — điểm nối giữa sidebar quick-list và trang Runs, cần agent gộp đối chiếu với file định nghĩa runs_table -- QUYET DINH: handler click của sidebar quick-list; có đọc self.runs_table nên cần API cầu nối sang co4e_run_control_widget.py khi tách, nhưng bản thân handler thuộc sidebar nên ở co4e_tab.py | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 621-629) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._section` | 627-659 | `ui/co4e_tab.py` | helper dựng UI section chung cho sidebar, ghi vào self._sections[key] | co | - |
|
||||
| `Co4ETab._fold_section` | 661-673 | `ui/co4e_tab.py` | đọc self._sections | co | - |
|
||||
| `Co4ETab._sync_section_arrow` | 675-678 | `ui/co4e_tab.py` | đọc self._sections | co | - |
|
||||
| `Co4ETab._icon_btn` | 679-683 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab._reload_sidebar` | 685-718 | `ui/co4e_tab.py` | [GOP 2 luot quet trung ky hieu dong 685] cắt ngang lát — cần agent gộp đối chiếu (thân method tiếp tục ngoài dòng 700); gọi co4e.list_workflows() nên chạm đĩa // cắt ngang lát — cần agent gộp đối chiếu (bắt đầu ở dòng 685, trước khoảng được giao 701-1400; phần thấy được chỉ là vòng lặp nạp custom agents + skills vào agent_list/skill_list) -- QUYET DINH: [GỘP 2 lượt quét trùng ký hiệu ở dòng 685] một agent đọc thân đến 700 (cắt ngang lát), agent kia đọc tới 718 (vòng lặp nạp agent/skill) — gộp thành 1 dòng 685-718; nạp lại toàn bộ sidebar, không phải trang Runs | co | - |
|
||||
| `Co4ETab._palette_item` | 720-724 | `ui/co4e_tab.py` | staticmethod tạo QListWidgetItem cho palette — không liên quan trang Runs -- QUYET DINH: factory QListWidgetItem cho palette — sidebar, không phải Runs | co | - |
|
||||
| `Co4ETab._build_center` | 727-867 | `ui/co4e_tab.py` | method dài >80 dòng, gộp nhiều việc không liên quan: dựng flow tab bar (QTabBar ẩn), gọi _build_runs_page() để nhét vào center_stack, dựng toolbar flow (name/add/save/mode/run/runs toggle), dựng canvas + overlay + splitter dọc với chat. Nên tách nhỏ. Gán self.center_stack — trạng thái chia sẻ dùng ở nhiều nơi (switch giữa trang Runs và flow editor, cả _show_runs ngoài lát này). -- QUYET DINH: dựng flow tab bar + toolbar + canvas/chat splitter; GỌI self._build_runs_page() để nhét vào center_stack — khi tách, chỗ gọi này đổi thành khởi tạo Co4ERunControlWidget(...) rồi add vào center_stack | co | - |
|
||||
| `Co4ETab.flow_bar` | 734 | `ui/co4e_tab.py` | QTabBar ẩn dùng làm index ánh xạ flow<->canvas, không phải trang Runs | co | - |
|
||||
| `Co4ETab.flow_add_btn` | 760 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab.flow_scroll` | 777 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab.center_stack` | 801 | `ui/co4e_tab.py` | trạng thái chia sẻ giữa trang Runs (stack 0) và flow editor (stack 1) — dùng bởi _show_runs (ngoài lát này) để chuyển trang -- QUYET DINH: container sở hữu QStackedWidget chứa [trang Runs, flow editor]; trang Runs (index 0) sẽ LÀ instance Co4ERunControlWidget được add vào đây | co | - |
|
||||
| `Co4ETab._build_runs_page` | 869-928 | `presentation/co4e/co4e_run_control_widget.py` | khớp đúng mô tả đích: dựng bảng runs_table + nút back/stop/rename/delete/clear/mở-thư-mục-workspace | co | - |
|
||||
| `Co4ETab.runs_back_btn` | 878 | `presentation/co4e/co4e_run_control_widget.py` | - | co | - |
|
||||
| `Co4ETab.runs_title` | 883 | `presentation/co4e/co4e_run_control_widget.py` | - | co | - |
|
||||
| `Co4ETab.ws_folder_btn` | 888 | `presentation/co4e/co4e_run_control_widget.py` | nút mở thư mục workspace — click gọi self._open_workspace_folder (định nghĩa ngoài lát này, có khả năng touches_disk_or_network=true) | co | - |
|
||||
| `Co4ETab.run_stop_btn` | 896 | `presentation/co4e/co4e_run_control_widget.py` | - | co | - |
|
||||
| `Co4ETab.run_rename_btn` | 901 | `presentation/co4e/co4e_run_control_widget.py` | - | co | - |
|
||||
| `Co4ETab.run_del_btn` | 905 | `presentation/co4e/co4e_run_control_widget.py` | - | co | - |
|
||||
| `Co4ETab.run_clear_btn` | 909 | `presentation/co4e/co4e_run_control_widget.py` | click gọi self.manager.clear_finished() — self.manager là trạng thái chia sẻ (run manager) không định nghĩa trong lát này | co | - |
|
||||
| `Co4ETab.runs_table` | 917 | `presentation/co4e/co4e_run_control_widget.py` | bảng chính của trang Runs; double-click gọi self._open_run_from_table, context menu gọi self._runs_context_menu (cả hai định nghĩa ngoài lát này) | co | - |
|
||||
| `Co4ETab._wrap_config` | 930-960 | `ui/co4e_tab.py` | wrap panel cấu hình step, không thuộc trang Runs -- QUYET DINH: khung bọc/thu-phóng quanh StepConfigPanel — thuộc bố cục của co4e_tab.py, không phải nội dung panel. Bảng cũ xếp cả cụm này vào node_property_panel.py (file cấm sửa của lane khác) — không đúng vai trò 'khung bọc' vs 'nội dung panel' | co | - |
|
||||
| `Co4ETab.config_toggle_btn` | 942 | `ui/co4e_tab.py` | thuộc khung bọc _wrap_config | co | - |
|
||||
| `Co4ETab.config_title` | 947 | `ui/co4e_tab.py` | thuộc khung bọc _wrap_config | co | - |
|
||||
| `Co4ETab._cfg_vlayout` | 953 | `ui/co4e_tab.py` | thuộc khung bọc _wrap_config | co | - |
|
||||
| `Co4ETab._cfg_top_spacer` | 957 | `ui/co4e_tab.py` | thuộc khung bọc _wrap_config | co | - |
|
||||
| `Co4ETab._cfg_bot_spacer` | 958 | `ui/co4e_tab.py` | thuộc khung bọc _wrap_config | co | - |
|
||||
| `Co4ETab.config_container` | 959 | `ui/co4e_tab.py` | thuộc khung bọc _wrap_config | co | - |
|
||||
| `Co4ETab._NARROW` | 967 | `ui/co4e_tab.py` | hằng số class-level, không liên quan trang Runs -- QUYET DINH: hằng số bố cục hẹp của cả tab | co | - |
|
||||
| `Co4ETab.showEvent` | 969-971 | `ui/co4e_tab.py` | Qt override, gắn narrow_guard -- QUYET DINH: [ANOMALY — 2 định nghĩa showEvent cùng tên trong 1 class, bảng cũ đã ghi nhận 'kept_separate_anomaly_symbols'] bản đầu (969-971) chỉ gắn narrow_guard, bị bản thứ 2 (1802) ghi đè lúc runtime; giữ TÁCH RIÊNG như bảng cũ | co | - |
|
||||
| `Co4ETab._apply_narrow_layout` | 973-983 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab._toggle_config` | 985-1028 | `ui/co4e_tab.py` | không thuộc trang Runs; đọc/ghi self._split (splitter 3 cột chia sẻ với _build_center/_wrap_config) | co | - |
|
||||
| `Co4ETab._refresh_min_width` | 1030-1035 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab._build_canvas_overlay` | 1037-1058 | `presentation/co4e/co4e_canvas_widget.py` | overlay zoom cho canvas — không thuộc trang Runs -- QUYET DINH: overlay zoom/fit gắn LÊN canvas qua self.canvas.add_overlay(); chấp nhận quyết định của bảng cũ — thuộc lane canvas, ngoài phạm vi run-control. Hiện tại canvas vẫn ở ui/co4e_canvas.py (chưa đổi tên), việc dời file này KHÔNG thuộc lane này | co | - |
|
||||
| `Co4ETab.zoom_in_btn` | 1050 | `presentation/co4e/co4e_canvas_widget.py` | nút overlay canvas — ngoài phạm vi run-control | co | - |
|
||||
| `Co4ETab.zoom_out_btn` | 1051 | `presentation/co4e/co4e_canvas_widget.py` | nút overlay canvas — ngoài phạm vi run-control | co | - |
|
||||
| `Co4ETab.fit_btn` | 1052 | `presentation/co4e/co4e_canvas_widget.py` | nút overlay canvas — ngoài phạm vi run-control | co | - |
|
||||
| `Co4ETab._build_chat` | 1060-1123 | `ui/co4e_tab.py` | method dài >60 dòng, dựng toàn bộ khung chat (header, stack theo flow, composer, routing toggle, usage total) — không thuộc trang Runs -- QUYET DINH: khung CHAT — thuộc lane co4e_chat_view.py (chưa tồn tại); giữ nguyên ui/co4e_tab.py cho tới khi lane đó tách, ngoài phạm vi lane run-control. Bảng cũ đã xếp sẵn vào presentation/co4e/co4e_chat_view.py | co | - |
|
||||
| `Co4ETab._chat_widget` | 1062 | `ui/co4e_tab.py` | khung CHAT — thuộc lane co4e_chat_view.py (chưa tồn tại); giữ nguyên ui/co4e_tab.py cho tới khi lane đó tách, ngoài phạm vi lane run-control. Bảng cũ đã xếp sẵn vào presentation/co4e/co4e_chat_view.py | co | - |
|
||||
| `Co4ETab._mhdr` | 1068 | `ui/co4e_tab.py` | khung CHAT — thuộc lane co4e_chat_view.py (chưa tồn tại); giữ nguyên ui/co4e_tab.py cho tới khi lane đó tách, ngoài phạm vi lane run-control. Bảng cũ đã xếp sẵn vào presentation/co4e/co4e_chat_view.py | co | - |
|
||||
| `Co4ETab.msgs_icon` | 1070 | `ui/co4e_tab.py` | khung CHAT — thuộc lane co4e_chat_view.py (chưa tồn tại); giữ nguyên ui/co4e_tab.py cho tới khi lane đó tách, ngoài phạm vi lane run-control. Bảng cũ đã xếp sẵn vào presentation/co4e/co4e_chat_view.py | co | - |
|
||||
| `Co4ETab.msgs_title` | 1071 | `ui/co4e_tab.py` | khung CHAT — thuộc lane co4e_chat_view.py (chưa tồn tại); giữ nguyên ui/co4e_tab.py cho tới khi lane đó tách, ngoài phạm vi lane run-control. Bảng cũ đã xếp sẵn vào presentation/co4e/co4e_chat_view.py | co | - |
|
||||
| `Co4ETab.chat_toggle_btn` | 1072 | `ui/co4e_tab.py` | khung CHAT — thuộc lane co4e_chat_view.py (chưa tồn tại); giữ nguyên ui/co4e_tab.py cho tới khi lane đó tách, ngoài phạm vi lane run-control. Bảng cũ đã xếp sẵn vào presentation/co4e/co4e_chat_view.py | co | - |
|
||||
| `Co4ETab.chat_stack` | 1087 | `ui/co4e_tab.py` | một ChatView mỗi flow — trạng thái chia sẻ đọc bởi _apply_workflow, _ensure_flow_log, _active_log -- QUYET DINH: khung CHAT — thuộc lane co4e_chat_view.py (chưa tồn tại); giữ nguyên ui/co4e_tab.py cho tới khi lane đó tách, ngoài phạm vi lane run-control. Bảng cũ đã xếp sẵn vào presentation/co4e/co4e_chat_view.py | co | - |
|
||||
| `Co4ETab._flow_logs` | 1088 | `ui/co4e_tab.py` | Dict[str, ChatView] theo wf.id — trạng thái chia sẻ giữa các tab flow -- QUYET DINH: khung CHAT — thuộc lane co4e_chat_view.py (chưa tồn tại); giữ nguyên ui/co4e_tab.py cho tới khi lane đó tách, ngoài phạm vi lane run-control. Bảng cũ đã xếp sẵn vào presentation/co4e/co4e_chat_view.py | co | - |
|
||||
| `Co4ETab.chat_input_row` | 1090 | `ui/co4e_tab.py` | khung CHAT — thuộc lane co4e_chat_view.py (chưa tồn tại); giữ nguyên ui/co4e_tab.py cho tới khi lane đó tách, ngoài phạm vi lane run-control. Bảng cũ đã xếp sẵn vào presentation/co4e/co4e_chat_view.py | co | - |
|
||||
| `Co4ETab._usage_total_lbl` | 1095 | `ui/co4e_tab.py` | khung CHAT — thuộc lane co4e_chat_view.py (chưa tồn tại); giữ nguyên ui/co4e_tab.py cho tới khi lane đó tách, ngoài phạm vi lane run-control. Bảng cũ đã xếp sẵn vào presentation/co4e/co4e_chat_view.py | co | - |
|
||||
| `Co4ETab.chat_input` | 1101 | `ui/co4e_tab.py` | khung CHAT — thuộc lane co4e_chat_view.py (chưa tồn tại); giữ nguyên ui/co4e_tab.py cho tới khi lane đó tách, ngoài phạm vi lane run-control. Bảng cũ đã xếp sẵn vào presentation/co4e/co4e_chat_view.py | co | - |
|
||||
| `Co4ETab.chat_send_btn` | 1104 | `ui/co4e_tab.py` | khung CHAT — thuộc lane co4e_chat_view.py (chưa tồn tại); giữ nguyên ui/co4e_tab.py cho tới khi lane đó tách, ngoài phạm vi lane run-control. Bảng cũ đã xếp sẵn vào presentation/co4e/co4e_chat_view.py | co | - |
|
||||
| `Co4ETab.co4e_routing_toggle` | 1109 | `ui/co4e_tab.py` | khung CHAT — thuộc lane co4e_chat_view.py (chưa tồn tại); giữ nguyên ui/co4e_tab.py cho tới khi lane đó tách, ngoài phạm vi lane run-control. Bảng cũ đã xếp sẵn vào presentation/co4e/co4e_chat_view.py | co | - |
|
||||
| `Co4ETab._co4e_routed_provider` | 1110 | `ui/co4e_tab.py` | override provider định tuyến cho lượt kế tiếp — trạng thái đọc/ghi ở nhiều nơi ngoài lát này (dòng 1849, 1871, 1878) -- QUYET DINH: khung CHAT — thuộc lane co4e_chat_view.py (chưa tồn tại); giữ nguyên ui/co4e_tab.py cho tới khi lane đó tách, ngoài phạm vi lane run-control. Bảng cũ đã xếp sẵn vào presentation/co4e/co4e_chat_view.py | co | - |
|
||||
| `Co4ETab._vsplit_sizes` | 1117 | `ui/co4e_tab.py` | khung CHAT — thuộc lane co4e_chat_view.py (chưa tồn tại); giữ nguyên ui/co4e_tab.py cho tới khi lane đó tách, ngoài phạm vi lane run-control. Bảng cũ đã xếp sẵn vào presentation/co4e/co4e_chat_view.py | co | - |
|
||||
| `Co4ETab._msgs_collapsed` | 1118 | `ui/co4e_tab.py` | khung CHAT — thuộc lane co4e_chat_view.py (chưa tồn tại); giữ nguyên ui/co4e_tab.py cho tới khi lane đó tách, ngoài phạm vi lane run-control. Bảng cũ đã xếp sẵn vào presentation/co4e/co4e_chat_view.py | co | - |
|
||||
| `Co4ETab._toggle_messages` | 1125-1157 | `ui/co4e_tab.py` | expand/collapse khung chat, đọc/ghi self._vsplit — không thuộc trang Runs -- QUYET DINH: khung CHAT — thuộc lane co4e_chat_view.py (chưa tồn tại); giữ nguyên ui/co4e_tab.py cho tới khi lane đó tách, ngoài phạm vi lane run-control. Bảng cũ đã xếp sẵn vào presentation/co4e/co4e_chat_view.py | co | - |
|
||||
| `Co4ETab._ensure_flow_log` | 1160-1169 | `ui/co4e_tab.py` | khung CHAT — thuộc lane co4e_chat_view.py (chưa tồn tại); giữ nguyên ui/co4e_tab.py cho tới khi lane đó tách, ngoài phạm vi lane run-control. Bảng cũ đã xếp sẵn vào presentation/co4e/co4e_chat_view.py | co | - |
|
||||
| `Co4ETab._active_log` | 1171-1173 | `ui/co4e_tab.py` | khung CHAT — thuộc lane co4e_chat_view.py (chưa tồn tại); giữ nguyên ui/co4e_tab.py cho tới khi lane đó tách, ngoài phạm vi lane run-control. Bảng cũ đã xếp sẵn vào presentation/co4e/co4e_chat_view.py | co | - |
|
||||
| `Co4ETab.chat_log` | 1175-1179 | `ui/co4e_tab.py` | property, không phải attribute gán trực tiếp — liệt kê vì là symbol công khai -- QUYET DINH: khung CHAT — thuộc lane co4e_chat_view.py (chưa tồn tại); giữ nguyên ui/co4e_tab.py cho tới khi lane đó tách, ngoài phạm vi lane run-control. Bảng cũ đã xếp sẵn vào presentation/co4e/co4e_chat_view.py | co | - |
|
||||
| `Co4ETab._plan_bubble` | 1181-1187 | `ui/co4e_tab.py` | property getter/setter, ủy quyền sang self._active_log()._co4e_plan_bubble -- QUYET DINH: khung CHAT — thuộc lane co4e_chat_view.py (chưa tồn tại); giữ nguyên ui/co4e_tab.py cho tới khi lane đó tách, ngoài phạm vi lane run-control. Bảng cũ đã xếp sẵn vào presentation/co4e/co4e_chat_view.py | co | - |
|
||||
| `Co4ETab._apply_workflow` | 1190-1204 | `ui/co4e_tab.py` | gán self._wf — trạng thái chia sẻ trung tâm của cả tab (đọc/ghi khắp nơi); gọi self.canvas.load/relayout/fit_view và self._update_run_btn/_refresh_usage_total (ngoài lát này) -- QUYET DINH: gán self._wf trung tâm — container | co | - |
|
||||
| `Co4ETab._new_workflow` | 1205-1213 | `ui/co4e_tab.py` | gọi self._open_flow (ngoài lát này) | co | - |
|
||||
| `Co4ETab._selected_wf` | 1215-1221 | `ui/co4e_tab.py` | co4e.get_workflow đọc storage — thuần logic sidebar, không thuộc trang Runs | co | - |
|
||||
| `Co4ETab._load_selected_workflow` | 1223-1226 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab._edit_selected_workflow` | 1228-1233 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab._duplicate_selected_workflow` | 1235-1242 | `application/workflows/co4e_workflow_service.py` | co4e.duplicate_workflow ghi storage -- QUYET DINH: đã có quyết định của lane workflow-service (bảng cũ); ngoài phạm vi run-control | co | - |
|
||||
| `Co4ETab._wf_context_menu` | 1244-1267 | `ui/co4e_tab.py` | menu chuột phải cho danh sách flow đã lưu, không phải trang Runs -- QUYET DINH: menu chuột phải cho danh sách flow đã lưu (sidebar) | co | - |
|
||||
| `Co4ETab._rename_workflow` | 1269-1285 | `application/workflows/co4e_workflow_service.py` | co4e.save_workflow ghi storage; đọc/ghi self._wf.name và self.name_edit -- QUYET DINH: đã có quyết định của lane workflow-service (bảng cũ); ngoài phạm vi run-control | co | - |
|
||||
| `Co4ETab._delete_selected_workflow` | 1287-1293 | `application/workflows/co4e_workflow_service.py` | co4e.delete_workflow ghi storage -- QUYET DINH: đã có quyết định của lane workflow-service (bảng cũ); ngoài phạm vi run-control | co | - |
|
||||
| `Co4ETab._sync_wf_from_canvas` | 1295-1298 | `ui/co4e_tab.py` | đọc self.canvas.nodes()/edges() và self.name_edit, ghi vào self._wf — trạng thái chia sẻ -- QUYET DINH: khớp bảng cũ | co | - |
|
||||
| `Co4ETab._save` | 1300-1305 | `application/workflows/co4e_workflow_service.py` | co4e.save_workflow ghi storage -- QUYET DINH: đã có quyết định của lane workflow-service (bảng cũ); ngoài phạm vi run-control | co | - |
|
||||
| `Co4ETab._autosave` | 1307-1310 | `application/workflows/co4e_workflow_service.py` | co4e.save_workflow ghi storage -- QUYET DINH: đã có quyết định của lane workflow-service (bảng cũ); ngoài phạm vi run-control | co | - |
|
||||
| `Co4ETab._on_name_changed` | 1312-1314 | `ui/co4e_tab.py` | gọi self._sync_active_flow_tab_text (ngoài lát này) | co | - |
|
||||
| `Co4ETab._add_blank_step` | 1316-1318 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab._on_node_selected` | 1321-1327 | `ui/co4e_tab.py` | đọc self.canvas.nodes(), self.config, self._config_collapsed -- QUYET DINH: đọc canvas selection rồi TOGGLE khung bọc _wrap_config (self._config_collapsed) — thuộc container, không phải nội dung StepConfigPanel. Bảng cũ xếp vào node_property_panel.py — không đúng vai trò khung bọc vs nội dung panel | co | - |
|
||||
| `Co4ETab._on_config_changed` | 1329-1332 | `ui/co4e_tab.py` | cùng lý do với _on_node_selected | co | - |
|
||||
| `Co4ETab._new_agent` | 1335-1336 | `presentation/co4e/agent_list_panel.py` | khớp bảng cũ, ngoài phạm vi run-control | co | - |
|
||||
| `Co4ETab._edit_agent` | 1338-1346 | `presentation/co4e/agent_list_panel.py` | khớp bảng cũ, ngoài phạm vi run-control | co | - |
|
||||
| `Co4ETab._edit_agent_dialog` | 1348-1354 | `presentation/co4e/agent_list_panel.py` | co4e.save_custom_agent ghi storage; mở dialog Qt -- QUYET DINH: khớp bảng cũ, ngoài phạm vi run-control | co | - |
|
||||
| `Co4ETab._delete_agent` | 1356-1363 | `presentation/co4e/agent_list_panel.py` | co4e.delete_custom_agent ghi storage -- QUYET DINH: khớp bảng cũ, ngoài phạm vi run-control | co | - |
|
||||
| `Co4ETab._manage_skills` | 1365-1369 | `presentation/co4e/skills_list_panel.py` | mở SkillsDialog (Qt) -- QUYET DINH: khớp bảng cũ, ngoài phạm vi run-control | co | - |
|
||||
| `Co4ETab._skill_map` | 1372-1378 | `application/workflows/co4e_workflow_service.py` | logic thuần, không phải trang Runs — thuộc nhóm run/mode bị loại trừ khỏi run_control_widget theo mô tả đích -- QUYET DINH: đã có quyết định của lane workflow-service (bảng cũ); ngoài phạm vi run-control | co | - |
|
||||
| `Co4ETab._current_mode` | 1380-1381 | `ui/co4e_tab.py` | thuộc mode/run toolbar — loại trừ khỏi run_control_widget theo mô tả đích -- QUYET DINH: đọc mode_combo của toolbar mode/run — mô tả đích loại trừ nhóm mode/run khỏi co4e_run_control_widget.py; bảng cũ xếp vào đó — mâu thuẫn với phạm vi hiện tại | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 1384-1385) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._on_mode_changed` | 1383-1389 | `ui/co4e_tab.py` | reset self._manual_active/_manual_order/_manual_idx — thuộc mode/run toolbar, loại trừ khỏi run_control_widget theo mô tả đích -- QUYET DINH: cùng nhóm mode/run toolbar — xem lý do ở _current_mode | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 1387-1393) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._on_run_clicked` | 1391-1400 | `ui/co4e_tab.py` | cắt ngang lát — cần agent gộp đối chiếu (thân method tiếp tục sau dòng 1400). Đây là logic thực thi run — loại trừ khỏi run_control_widget theo mô tả đích; gọi self.manager.stop (trạng thái chia sẻ run manager) -- QUYET DINH: logic bấm nút Run (thực thi) — cùng nhóm bị loại trừ, xem lý do ở _current_mode | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 1395-1400) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._start_canvas_run` | 1403-1420 | `ui/co4e_tab.py` | logic khởi chạy run trên canvas (không phải bảng Flow Status); đọc/ghi self._wf, self._flows tiles, self._flow_runs, self._run_logs — trạng thái chia sẻ với _on_manager_event và _manual_step | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 1407-1424) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._plan_bubble` | 1413 | `ui/co4e_tab.py` | gán lại None khi bắt đầu run mới; cũng gán ở _manual_run_or_advance (1457) và đọc/ghi trong _append_plan qua log._co4e_plan_bubble — có thể đã khởi tạo lần đầu ở __init__ ngoài phạm vi đọc | co (nhung o dong khac: 1186-1187) | - |
|
||||
| `Co4ETab._run_single` | 1422-1427 | `ui/co4e_tab.py` | - | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 1426-1431) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._run_from` | 1429-1433 | `ui/co4e_tab.py` | - | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 1433-1437) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._downstream` | 1435-1446 | `ui/co4e_tab.py` | đọc self.canvas.edges() — chỉ cần canvas object tồn tại, không cần app hiển thị; logic đồ thị thuần | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 1439-1450) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._manual_run_or_advance` | 1449-1462 | `ui/co4e_tab.py` | khởi tạo self._manual_order/_manual_idx/_manual_active — trạng thái mode thủ công, không liên quan bảng Flow Status | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 1453-1466) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._manual_order` | 1458 | `ui/co4e_tab.py` | trạng thái mode thủ công, dùng bởi _manual_step; có thể đã khởi tạo ở __init__ ngoài phạm vi đọc | co (nhung o dong khac: 253) | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 253) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._manual_idx` | 1459 | `ui/co4e_tab.py` | trạng thái mode thủ công, dùng bởi _manual_step/_on_manager_event | co (nhung o dong khac: 254) | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 254) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._manual_active` | 1460 | `ui/co4e_tab.py` | trạng thái mode thủ công, đọc bởi _on_manager_event | co (nhung o dong khac: 252) | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 252) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._manual_step` | 1464-1480 | `ui/co4e_tab.py` | ghi self._flow_runs[wf.id] và self._run_logs[run_id] — dict chia sẻ với _start_canvas_run/_on_manager_event | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 1468-1484) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._topo_order` | 1482-1487 | `ui/co4e_tab.py` | đọc self.canvas.nodes()/edges() — thuần logic sắp xếp topo | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 1486-1491) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._on_manager_event` | 1490-1546 | `ui/co4e_tab.py` | method gộp nhiều việc không liên quan: routing event theo loại (node_status/node_output/node_diff/node_plan/node_tool/run_done/error), cập nhật canvas, ghi self._outputs_for, dọn self._flow_runs/self._run_logs, gọi popup thông báo — nên tách nhỏ thêm dù chưa vượt 80 dòng. KHÔNG thuộc bảng Flow Status (không đụng runs_table trực tiếp), là logic thực thi run nên ở lại co4e_tab.py theo _doc | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 1494-1550) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._notify_run_finished` | 1548-1569 | `ui/co4e_tab.py` | tạo QMessageBox không chặn, ghi self._run_popups — popup thông báo chung, không phải phần bảng Flow Status | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 1552-1573) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._run_popups` | 1557 | `ui/co4e_tab.py` | danh sách giữ tham chiếu QMessageBox không-chặn để tránh bị GC — khởi tạo có điều kiện (hasattr) ngay trong _notify_run_finished thay vì __init__ | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 1560-1561) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._refresh_runs` | 1571-1612 | `presentation/co4e/co4e_run_control_widget.py` | dựng lại bảng self.runs_table từ manager — đúng lõi 'Flow Status'; cũng đụng self._sections và self.flow_bar (tab text 'RUNS N') và gọi self._refresh_side_runs() — trạng thái chia sẻ với sidebar/tab strip ngoài phạm vi widget này, cần API cầu nối khi tách | co | - |
|
||||
| `Co4ETab._stop_selected_run` | 1614-1620 | `presentation/co4e/co4e_run_control_widget.py` | nút Stop của bảng Flow Status | co | - |
|
||||
| `Co4ETab._delete_selected_run` | 1622-1635 | `presentation/co4e/co4e_run_control_widget.py` | nút Delete; ghi self._flow_runs.pop/self._run_logs.pop — dict chia sẻ với logic thực thi run ở co4e_tab.py | co | - |
|
||||
| `Co4ETab._runs_context_menu` | 1637-1651 | `presentation/co4e/co4e_run_control_widget.py` | context menu của runs_table (open run/output/rename/delete); action 'open_run' gọi self._open_run_from_table vốn cần self._open_flow + self.canvas (ở co4e_tab.py) — cần callback cầu nối khi tách | co | - |
|
||||
| `Co4ETab.set_project` | 1654-1670 | `ui/co4e_tab.py` | đọc/ghi self._project_id, self._project_dir; gọi load_project (đọc đĩa) và manager.set_output_root/set_current_project; gọi self._refresh_ws_folder_btn() thuộc widget Flow Status — cần API cầu nối khi tách ra co4e_run_control_widget.py | co | - |
|
||||
| `Co4ETab._project_id` | 1659 | `ui/co4e_tab.py` | trạng thái workspace hiện tại, dùng bởi manager.set_current_project và các hàm mở thư mục — chia sẻ giữa co4e_tab.py và co4e_run_control_widget.py | co (nhung o dong khac: 249) | - |
|
||||
| `Co4ETab._project_dir` | 1660 | `ui/co4e_tab.py` | dùng bởi _flow_output_root — ảnh hưởng trực tiếp tới nút 'mở thư mục workspace' của Flow Status | co (nhung o dong khac: 250) | - |
|
||||
| `Co4ETab._flow_output_root` | 1672-1682 | `ui/co4e_tab.py` | helper Path dùng chung bởi cả chat (_out_dir) và nút 'mở thư mục workspace' của Flow Status (_open_workspace_folder/_refresh_ws_folder_btn) — widget Flow Status cần được truyền hàm này qua callback/property thay vì tự tính | co | - |
|
||||
| `Co4ETab._refresh_ws_folder_btn` | 1684-1689 | `presentation/co4e/co4e_run_control_widget.py` | cập nhật self.ws_folder_btn (nút mở thư mục workspace) — phụ thuộc self._flow_output_root() ở co4e_tab.py | co | - |
|
||||
| `Co4ETab._open_workspace_folder` | 1691-1698 | `presentation/co4e/co4e_run_control_widget.py` | hành động của nút 'mở thư mục workspace'; mkdir + open_location (spawn process) | co | - |
|
||||
| `Co4ETab._open_run_output_folder` | 1700-1711 | `presentation/co4e/co4e_run_control_widget.py` | mở thư mục output của 1 run cụ thể — dùng trong context menu bảng Flow Status; mkdir + open_location | co | - |
|
||||
| `Co4ETab._rename_selected_run` | 1713-1744 | `presentation/co4e/co4e_run_control_widget.py` | nút Rename của bảng Flow Status; nhưng đồng bộ qua self._flows, self.flow_bar.setTabText, self.name_edit — trạng thái tab/canvas chia sẻ với co4e_tab.py, cần callback cầu nối; co4e.save_workflow ghi đĩa | co | - |
|
||||
| `Co4ETab._run_selected_in_background` | 1746-1756 | `ui/co4e_tab.py` | chạy 1 flow được chọn (self._selected_wf(), không rõ nguồn — có thể là danh sách sidebar chứ không phải runs_table); gọi self._refresh_side_runs() — không khớp mô tả 'CHỈ bảng Flow Status' nên giữ ở co4e_tab.py | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 1750-1760) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._wf_by_id` | 1758-1766 | `ui/co4e_tab.py` | co4e.get_workflow đọc file workflow đã lưu trên đĩa; helper dùng chung nhiều nơi (rerun, open-from-table) | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 1762-1770) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._rerun_run_item` | 1768-1779 | `ui/co4e_tab.py` | docstring nói 'double-click a run in the history' nhưng item.data(Qt.UserRole) trực tiếp gợi ý đây là list sidebar 'side runs', KHÔNG phải bảng Flow Status (khác cách _open_run_from_table truy cập runs_table qua item.row()) — cần người quyết đây thuộc trang nào -- QUYET DINH: gọi self.manager.start(...) tức THỰC THI run (network/AI) — cùng nhóm run-execution bị loại trừ khỏi co4e_run_control_widget.py; bảng cũ xếp vào đó. Nghi vấn về nguồn item (sidebar hay bảng) không đổi kết luận vì lý do loại trừ là do HÀNH VI thực thi chứ không phải nơi click | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 1772-1783) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab._open_run_from_table` | 1781-1800 | `ui/co4e_tab.py` | double-click bảng Flow Status nhưng gọi self._open_flow (chuyển tab) và self.canvas.update_node_status trực tiếp — vượt ra ngoài phạm vi 'chỉ bảng runs_table' nên giữ ở co4e_tab.py, cần callback nếu tách runs_table riêng | co | BANG CU SAI/QUA RONG: xep vao presentation/co4e/co4e_run_control_widget.py (dong 1785-1804) nhung pham vi run-control hien tai (mo ta dich cua 3 agent quet) loai tru nhom nay -> quyet dinh o day = ui/co4e_tab.py. |
|
||||
| `Co4ETab.showEvent` | 1802-1805 | `ui/co4e_tab.py` | override Qt lifecycle của chính Co4ETab, gọi self._refresh_runs() — phải ở lại widget chính | co | - |
|
||||
| `Co4ETab._out_dir` | 1807-1813 | `ui/co4e_tab.py` | mkdir; dùng cho chat/flow deliverables, không liên quan bảng Flow Status | co | - |
|
||||
| `Co4ETab._chat_send` | 1816-1842 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab._apply_co4e_routing` | 1844-1879 | `ui/co4e_tab.py` | ghi self._co4e_routed_provider; nhánh 'manual' dựng dialog confirm_switch(self,...) nên cần widget sống | co | - |
|
||||
| `Co4ETab._co4e_routed_provider` | 1849 | `ui/co4e_tab.py` | dùng bởi _run_chat_turn để chọn provider — reset mỗi lần _apply_co4e_routing chạy | co | - |
|
||||
| `Co4ETab._extract_agent_directive` | 1881-1887 | `ui/co4e_tab.py` | thuần regex, test được không cần Qt | co | - |
|
||||
| `Co4ETab._resolve_agent` | 1889-1897 | `ui/co4e_tab.py` | co4e.list_custom_agents() đọc dữ liệu agent tuỳ biến đã lưu | co | - |
|
||||
| `Co4ETab._run_chat_turn` | 1899-1970 | `ui/co4e_tab.py` | method dài, gộp: dựng prompt, chạy AgentWorker nền gọi provider AI (network), stream sự kiện vào bubble Qt, cập nhật usage — nên tách nhỏ thêm dù chỉ 72 dòng vì nhiều trách nhiệm khác nhau (build prompt / worker job / event routing / usage). Không liên quan bảng Flow Status | co | - |
|
||||
| `Co4ETab._chat_worker` | 1953 | `ui/co4e_tab.py` | vị trí xuất hiện đầu tiên theo văn bản là trong hàm lồng 'done'; gán thật sự lúc chạy là dòng 1969 (w = AgentWorker...); có thể đã khởi tạo None ở __init__ ngoài phạm vi đọc | co (nhung o dong khac: 236) | - |
|
||||
| `Co4ETab._append_chat` | 1972-1987 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab._fmt_usage` | 1990-1997 | `ui/co4e_tab.py` | thuần formatting, không cần Qt | co | - |
|
||||
| `Co4ETab._apply_usage` | 1999-2016 | `ui/co4e_tab.py` | ghi self._flow_usage[wf_id] — tổng usage theo flow, chia sẻ với _refresh_usage_total | co | - |
|
||||
| `Co4ETab._refresh_usage_total` | 2018-2033 | `ui/co4e_tab.py` | đọc self._flow_usage và self._wf — trạng thái chia sẻ với _apply_usage | co | - |
|
||||
| `Co4ETab._append_diff` | 2035-2039 | `ui/co4e_tab.py` | - | co | - |
|
||||
| `Co4ETab._append_plan` | 2041-2052 | `ui/co4e_tab.py` | ghi log._co4e_plan_bubble — thuộc tính gắn động lên ChatView, chia sẻ với _run_chat_turn | co | - |
|
||||
| `Co4ETab._retranslate` | 2055-2075 | `ui/co4e_tab.py` | gộp retranslate cho CẢ widget chung (wf_new_btn, self._sections) LẪN các nút riêng của Flow Status (runs_back_btn, run_stop_btn, run_rename_btn, run_del_btn, run_clear_btn, runs_table headers, ws_folder_btn) — khi tách co4e_run_control_widget.py cần chia method này làm hai, phần Flow Status nên có retranslate riêng gọi từ đây | co | - |
|
||||
| `_html_escape` | 2078-2079 | `ui/co4e_tab.py` | hàm module-level thuần string escape; không thấy nơi gọi trong phạm vi 1401-2084 nên chưa rõ nó phục vụ phần nào — có thể dùng ở phần chat render ngoài phạm vi đọc -- QUYET DINH: escape string thuần cho phần render CHAT — lane co4e_chat_view.py (chưa tồn tại); bảng cũ đã xếp sẵn vào đó, ngoài phạm vi run-control nên giữ nguyên vị trí vật lý hiện tại | co | - |
|
||||
| `_qcolor` | 2082-2084 | `presentation/co4e/co4e_run_control_widget.py` | QColor là kiểu giá trị (như QPointF/QRectF), không cần QApplication sống; chỉ thấy dùng trong _refresh_runs (tô màu cột status của bảng Flow Status) trong phạm vi đọc | co | - |
|
||||
|
||||
## Chỗ thấy khác tài liệu — cần người quyết
|
||||
|
||||
Không có symbol nào trong 215 symbol thô có note bắt đầu bằng `khac tai lieu:` — mục này để trống theo đúng yêu cầu tự kiểm (không có gì cần liệt kê).
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,407 @@
|
||||
# Bản đồ tách file Co4E (gộp từ 6 agent quét song song)
|
||||
|
||||
- Tổng số symbol quét được (thô, tính cả trùng): **367**
|
||||
- Tổng số dòng trong bản đồ cuối cùng (sau gộp trùng): **363**
|
||||
- Số nhóm ký hiệu bị quét trùng bởi 2 agent (đã gộp làm 1 dòng): 4 (Co4ECanvas.dropEvent, Co4ETab._co4e_routed_provider, Co4ETab._config_expanded_w, Co4ETab._reload_sidebar)
|
||||
- Số ký hiệu có 2 định nghĩa thật ở 2 dòng khác nhau, giữ riêng và gắn cờ: 1 (Co4ETab.showEvent)
|
||||
- EXPECTED (tools/check_co4e.py) có **27** phần tử; kết quả đối chiếu: OK — cả 27 xuất hiện đúng 1 lần, có target cụ thể
|
||||
- Số note bắt đầu bằng `khac tai lieu:`: **0** (không có -> không có mục 'Chỗ thấy khác tài liệu' nào phát sinh từ tiêu chí này)
|
||||
|
||||
## Ghi chú về gộp trùng (5 ký hiệu bị 2 agent cùng quét thấy)
|
||||
|
||||
### `Co4ETab._config_expanded_w`
|
||||
- dòng 275-275, target agent gốc đề xuất = `presentation/co4e/co4e_tab.py` — cùng lý do với _config_collapsed
|
||||
- dòng 995-995, target agent gốc đề xuất = `presentation/co4e/co4e_tab.py` — gán trong nhánh collapse của _toggle_config; có thể đã khởi tạo trong __init__ (ngoài phạm vi đọc).
|
||||
- **Quyết định gộp**: 1 dòng, target cuối = `presentation/co4e/co4e_tab.py`, dòng 275; 995
|
||||
|
||||
### `Co4ETab._reload_sidebar`
|
||||
- dòng 689-700, target agent gốc đề xuất = `presentation/co4e/co4e_tab.py` — cắt ngang lát — cần agent gộp đối chiếu (method tiếp tục sau dòng 700); gọi co4e.list_workflows() (đọc đĩa) rồi dựng QListWidgetItem trực tiếp trong cùng hàm — trộn data-fetch và UI, nên tách phần đọc dữ liệu sang co4e_workflow_service.py
|
||||
- dòng 689-722, target agent gốc đề xuất = `unsure` — cắt ngang lát — cần agent gộp đối chiếu (bắt đầu trước dòng 701). Gộp refresh cả agent_list và skill_list trong cùng 1 hàm — nên tách thành _refresh_agents_list (agent_list_panel.py) và _refresh_skills_list (skills_list_panel.py) như plan.md yêu cầu; gọi co4e.list_custom_agents() (đĩa) và skills_mod.skill_prefix_for (đĩa).
|
||||
- **Quyết định gộp**: 1 dòng, target cuối = `presentation/co4e/co4e_tab.py`, dòng 689-722
|
||||
|
||||
### `Co4ETab.showEvent`
|
||||
- dòng 973-975, target agent gốc đề xuất = `presentation/co4e/co4e_tab.py` — Qt override, gọi self._narrow_guard.attach() (đối tượng khởi tạo ngoài phạm vi đọc, <701).
|
||||
- dòng 1806-1809, target agent gốc đề xuất = `presentation/co4e/co4e_tab.py` — override Qt lifecycle method của chính QWidget container nên phải ở lại co4e_tab.py; gọi self._refresh_runs() (thuộc co4e_run_control_widget.py) — điểm nối giữa container và run-control widget.
|
||||
- **Quyết định**: đây là 2 định nghĩa method trùng tên thật sự tồn tại ở 2 vị trí khác nhau trong cùng class `Co4ETab` (khả năng cao là lỗi nguồn — định nghĩa sau đè định nghĩa trước, làm dòng logic ở định nghĩa trước thành dead code). Giữ nguyên 2 dòng riêng biệt trong bản đồ, KHÔNG gộp, và cần người quyết định giữ định nghĩa nào.
|
||||
|
||||
### `Co4ETab._co4e_routed_provider`
|
||||
- dòng 1114-1114, target agent gốc đề xuất = `presentation/co4e/co4e_chat_view.py` — override provider cho lượt chat kế tiếp — dùng bởi _apply_co4e_routing (ngoài phạm vi đọc, >1400).
|
||||
- dòng 1853-1853, target agent gốc đề xuất = `presentation/co4e/co4e_chat_view.py` — gán None lần đầu tại đây (trong _apply_co4e_routing); đọc bằng getattr(self, '_co4e_routed_provider', None) ở _run_chat_turn dòng ~1922 — gợi ý có thể không được init trong __init__. Trạng thái routing theo từng lượt chat, dùng chung giữa chat-view và _start_canvas_run/manager.start.
|
||||
- **Quyết định gộp**: 1 dòng, target cuối = `presentation/co4e/co4e_chat_view.py`, dòng 1114; 1853
|
||||
|
||||
### `Co4ECanvas.dropEvent`
|
||||
- dòng 683-700, target agent gốc đề xuất = `presentation/co4e/co4e_canvas_widget.py` — cắt ngang lát — cần agent gộp đối chiếu (thân try chưa đóng ở dòng 700, còn tiếp; xử lý JSON từ mimeData trong bộ nhớ, không phải file đĩa nên touches_disk_or_network=false)
|
||||
- dòng 683-701, target agent gốc đề xuất = `presentation/co4e/co4e_canvas_widget.py` — cắt ngang lát — cần agent gộp đối chiếu. Method dropEvent bắt đầu ở dòng 683 (ngoài khoảng được giao 701-791), chỉ dòng 701 (e.acceptProposedAction()) nằm trong lát này; dòng 702 là dòng trống cuối file. File ui/co4e_canvas.py chỉ có 702 dòng — không có nội dung nào khác trong khoảng 702-791 vì đã hết file. Xử lý drop từ sidebar (kind=='workflow' → add_workflow, ngược lại → add_palette_step) — logic thao tác node/canvas nên khớp nhóm _add_node/_connect_nodes trong bảng plan.md dòng 623 dù plan.md không liệt kê rõ dropEvent.
|
||||
- **Quyết định gộp**: 1 dòng, target cuối = `presentation/co4e/co4e_canvas_widget.py`, dòng 683-701
|
||||
|
||||
## Bảng đầy đủ
|
||||
|
||||
| symbol | dòng | file đích | lý do | có trong bảng cũ chưa | chỗ nào thấy bảng cũ sai |
|
||||
|---|---|---|---|---|---|
|
||||
| Co4ETab._flow_outputs | 245 | application/workflows/co4e_workflow_service.py | state per-flow outputs — ứng viên chuyển vào state machine thuần Python | chua/khong ro | - |
|
||||
| Co4ETab._outputs_for | 475-478 | application/workflows/co4e_workflow_service.py | truy cập dict state per-flow outputs, thuần Python | chua/khong ro | - |
|
||||
| Co4ETab._duplicate_selected_workflow | 1239-1246 | application/workflows/co4e_workflow_service.py | tương ứng _duplicate_flow() trong bảng plan.md dòng 616, nhưng tên hàm thực tế khác (_duplicate_selected_workflow). Gộp gọi service (co4e.duplicate_workflow, đĩa) với cập nhật UI (self._reload_sidebar(), self.status_message.emit) — cần tách; phần UI nên ở lại co4e_tab.py. | co | ate_flow() trong bảng plan.md dòng 616, nhưng tên hàm thực tế khác (_duplicate_selected_workflow). Gộp gọi service (co4e.duplicate_workflow, đĩa) |
|
||||
| Co4ETab._rename_workflow | 1273-1289 | application/workflows/co4e_workflow_service.py | mở QInputDialog (cần Qt) rồi gọi co4e.save_workflow (đĩa), đồng bộ self._wf.name/self.name_edit nếu flow đang mở là flow bị đổi tên — gộp UI dialog + service + trạng thái chia sẻ self._wf trong 1 hàm, nên tách. | chua/khong ro | - |
|
||||
| Co4ETab._delete_selected_workflow | 1291-1297 | application/workflows/co4e_workflow_service.py | tương ứng _delete_flow() trong bảng plan.md dòng 616; gọi co4e.delete_workflow (đĩa). | co | - |
|
||||
| Co4ETab._save | 1304-1309 | application/workflows/co4e_workflow_service.py | gộp _sync_wf_from_canvas (đọc canvas UI), lưu đĩa (co4e.save_workflow), và cập nhật UI (_reload_sidebar, status_message) — cần tách phần service khỏi phần UI khi chuyển sang co4e_workflow_service.py. | chua/khong ro | - |
|
||||
| Co4ETab._autosave | 1311-1314 | application/workflows/co4e_workflow_service.py | gọi self._sync_wf_from_canvas() (cần canvas) rồi co4e.get_workflow/save_workflow (đĩa). | chua/khong ro | - |
|
||||
| Co4ETab._skill_map | 1376-1382 | application/workflows/co4e_workflow_service.py | chuẩn bị nội dung skill (skills_mod.skill_prefix_for, đọc đĩa) để đưa vào lúc chạy flow/chat — không có trong bảng plan.md, tự xếp theo chức năng (dùng lúc run/build system prompt, không phải UI danh sách skill). | co | ọc đĩa) để đưa vào lúc chạy flow/chat — không có trong bảng plan.md, tự xếp theo chức năng (dùng lúc run/build system prompt, không phải UI danh sá |
|
||||
| Co4ETab.ag_new_btn | 550 | presentation/co4e/agent_list_panel.py | hiện định nghĩa trực tiếp trong Co4ETab._build_sidebar — ứng viên chuyển sang agent_list_panel.py tương tự cách skills đã tách | chua/khong ro | - |
|
||||
| Co4ETab.agent_list | 558 | presentation/co4e/agent_list_panel.py | cùng lý do với ag_new_btn | chua/khong ro | - |
|
||||
| Co4ETab.ag_edit_btn | 562 | presentation/co4e/agent_list_panel.py | cùng lý do với ag_new_btn | chua/khong ro | - |
|
||||
| Co4ETab.ag_del_btn | 563 | presentation/co4e/agent_list_panel.py | cùng lý do với ag_new_btn | chua/khong ro | - |
|
||||
| Co4ETab._new_agent | 1339-1340 | presentation/co4e/agent_list_panel.py | tương ứng _create_agent() trong bảng plan.md dòng 620 (tên hàm thực tế là _new_agent). | co | - |
|
||||
| Co4ETab._edit_agent | 1342-1350 | presentation/co4e/agent_list_panel.py | khớp _edit_agent() dòng 620 plan.md; gọi co4e.list_custom_agents() (đĩa). | co | - |
|
||||
| Co4ETab._edit_agent_dialog | 1352-1358 | presentation/co4e/agent_list_panel.py | mở Co4EAgentDialog rồi co4e.save_custom_agent (đĩa) và self._reload_sidebar() — trạng thái chia sẻ, liên quan tới cắt ngang lát ở _reload_sidebar. | chua/khong ro | - |
|
||||
| Co4ETab._delete_agent | 1360-1367 | presentation/co4e/agent_list_panel.py | khớp _delete_agent() dòng 620 plan.md; gọi co4e.delete_custom_agent (đĩa). | co | - |
|
||||
| _status_color | 43-50 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| _NodeItem | 59-210 | presentation/co4e/co4e_canvas_widget.py | class nội bộ gắn chặt với Co4ECanvas qua self.canvas backreference — nên đi cùng file với Co4ECanvas | chua/khong ro | - |
|
||||
| _NodeItem.__init__ | 62-72 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| _NodeItem.node | 64 | presentation/co4e/co4e_canvas_widget.py | giữ tham chiếu domain Node — dữ liệu chia sẻ với core/co4e.py | chua/khong ro | - |
|
||||
| _NodeItem.canvas | 65 | presentation/co4e/co4e_canvas_widget.py | backreference tới Co4ECanvas cha — mọi event của _NodeItem đều gọi ngược lên canvas (add_step_below, begin_connect, delete_node, các signal) — điểm khớp nối chặt nhất trong file | chua/khong ro | - |
|
||||
| _NodeItem.status | 66 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| _NodeItem._porting | 67 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| _NodeItem.boundingRect | 74-76 | presentation/co4e/co4e_canvas_widget.py | trả QRectF như kiểu giá trị, logic thuần | chua/khong ro | - |
|
||||
| _NodeItem._card_rect | 78-79 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| _NodeItem.paint | 81-139 | presentation/co4e/co4e_canvas_widget.py | vẽ toàn bộ card: nền, header stripe, label, badge role, preview instructions/sub-agents, footer model/skills, 2 port — gộp nhiều việc nhưng vẫn dưới 80 dòng nên chưa bắt buộc tách thêm | chua/khong ro | - |
|
||||
| _NodeItem._in_out_port | 141-143 | presentation/co4e/co4e_canvas_widget.py | hình học thuần dùng QPointF như kiểu giá trị | chua/khong ro | - |
|
||||
| _NodeItem.itemChange | 145-156 | presentation/co4e/co4e_canvas_widget.py | ghi self.node.x/y rồi gọi self.canvas._reposition_edges() và emit self.canvas.graph_changed/node_selected — chạm trạng thái chia sẻ của canvas cha | chua/khong ro | - |
|
||||
| _NodeItem.hoverMoveEvent | 158-161 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| _NodeItem.mousePressEvent | 163-174 | presentation/co4e/co4e_canvas_widget.py | đọc/ghi self.canvas._connect_from, gọi canvas._finish_connect/begin_port_drag — trạng thái connect-mode chia sẻ với Co4ECanvas | chua/khong ro | - |
|
||||
| _NodeItem.mouseMoveEvent | 176-181 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| _NodeItem.mouseReleaseEvent | 183-189 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| _NodeItem.mouseDoubleClickEvent | 191-193 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| _NodeItem.contextMenuEvent | 195-207 | presentation/co4e/co4e_canvas_widget.py | gọi canvas.add_step_below/begin_connect/delete_node — trạng thái/hành vi thuộc canvas cha | chua/khong ro | - |
|
||||
| _NodeItem.center | 209-210 | presentation/co4e/co4e_canvas_widget.py | trả QPointF như kiểu giá trị | chua/khong ro | - |
|
||||
| _EdgeItem | 213-286 | presentation/co4e/co4e_canvas_widget.py | class nội bộ gắn chặt với Co4ECanvas qua self.canvas backreference | chua/khong ro | - |
|
||||
| _EdgeItem.__init__ | 214-225 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| _EdgeItem.edge | 216 | presentation/co4e/co4e_canvas_widget.py | tham chiếu domain Edge — chia sẻ với core/co4e.py | chua/khong ro | - |
|
||||
| _EdgeItem.canvas | 217 | presentation/co4e/co4e_canvas_widget.py | backreference tới Co4ECanvas — contextMenuEvent gọi canvas.delete_edge | chua/khong ro | - |
|
||||
| _EdgeItem._dst | 218 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| _EdgeItem._hover | 224 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| _EdgeItem._apply_pen | 227-235 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| _EdgeItem.update_path | 237-239 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| _EdgeItem.boundingRect | 241-242 | presentation/co4e/co4e_canvas_widget.py | gọi super().boundingRect() phụ thuộc trạng thái path sống của item | chua/khong ro | - |
|
||||
| _EdgeItem.shape | 244-249 | presentation/co4e/co4e_canvas_widget.py | dùng QPainterPathStroker trên self.path() sống của item | chua/khong ro | - |
|
||||
| _EdgeItem.hoverEnterEvent | 251-255 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| _EdgeItem.hoverLeaveEvent | 257-261 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| _EdgeItem.paint | 263-279 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| _EdgeItem.contextMenuEvent | 281-286 | presentation/co4e/co4e_canvas_widget.py | gọi self.canvas.delete_edge(self.edge) | chua/khong ro | - |
|
||||
| Co4ECanvas | 289-700 | presentation/co4e/co4e_canvas_widget.py | cắt ngang lát — class tiếp tục sau dòng 700 (dropEvent chưa kết thúc, có thể còn method khác chưa đọc) — cần agent gộp đối chiếu với phần đọc dòng 701+ | chua/khong ro | - |
|
||||
| Co4ECanvas.node_selected | 290 | presentation/co4e/co4e_canvas_widget.py | Signal được node_property_panel.py (và co4e_tab.py) nối vào để nạp node được chọn — điểm chia sẻ giữa canvas và property panel | chua/khong ro | - |
|
||||
| Co4ECanvas.node_activated | 291 | presentation/co4e/co4e_canvas_widget.py | Signal double-click, có thể được co4e_tab.py nối để mở panel chỉnh sửa — cần kiểm nơi consume | chua/khong ro | - |
|
||||
| Co4ECanvas.graph_changed | 292 | presentation/co4e/co4e_canvas_widget.py | Signal báo graph đổi (autosave) — nhiều khả năng được co4e_tab.py/co4e_workflow_service.py nối để lưu flow, trạng thái chia sẻ xuyên lớp application | chua/khong ro | - |
|
||||
| Co4ECanvas.__init__ | 296-315 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas._scene | 299 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas._nodes | 305 | presentation/co4e/co4e_canvas_widget.py | dict id->_NodeItem — trạng thái trung tâm được đọc/ghi bởi gần như mọi method của Co4ECanvas (add/delete/relayout/zoom/status/route edges) | chua/khong ro | - |
|
||||
| Co4ECanvas._edges | 306 | presentation/co4e/co4e_canvas_widget.py | list _EdgeItem — trạng thái trung tâm tương tự self._nodes | chua/khong ro | - |
|
||||
| Co4ECanvas._connect_from | 307 | presentation/co4e/co4e_canvas_widget.py | trạng thái connect-mode, cũng được _NodeItem.mousePressEvent đọc/ghi qua self.canvas | chua/khong ro | - |
|
||||
| Co4ECanvas._zoom | 308 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas._panning | 309 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas._pan_start | 310 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas._overlay | 311 | presentation/co4e/co4e_canvas_widget.py | widget zoom/fit overlay được co4e_tab.py hoặc co4e_canvas_widget.py truyền vào qua add_overlay() | chua/khong ro | - |
|
||||
| Co4ECanvas._port_src | 313 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas._port_src_pt | 314 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas._temp_edge | 315 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas.add_overlay | 318-323 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas._place_overlay | 325-330 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas.resizeEvent | 332-334 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas.scrollContentsBy | 336-341 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas.showEvent | 343-345 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas.load | 348-362 | presentation/co4e/co4e_canvas_widget.py | reset toàn bộ self._nodes/self._edges/self._connect_from/self._port_src/self._temp_edge — điểm nạp lại state từ flow, được co4e_tab.py hoặc co4e_workflow_service.py gọi khi mở flow | chua/khong ro | - |
|
||||
| Co4ECanvas.nodes | 364-365 | presentation/co4e/co4e_canvas_widget.py | chỉ đọc .node từ self._nodes, không gọi API Qt trực tiếp | chua/khong ro | - |
|
||||
| Co4ECanvas.edges | 367-368 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas.add_node | 371-382 | presentation/co4e/co4e_canvas_widget.py | ghi self._nodes, scene.addItem, emit graph_changed/node_selected — trạng thái chia sẻ với property panel qua node_selected | chua/khong ro | - |
|
||||
| Co4ECanvas.add_step_below | 384-390 | presentation/co4e/co4e_canvas_widget.py | orchestration thuần, ủy quyền cho add_node (bản thân không gọi trực tiếp API Qt) | chua/khong ro | - |
|
||||
| Co4ECanvas._chain_tail | 392-396 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas.add_palette_step | 398-400 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas.begin_connect | 402-403 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas._finish_connect | 405-409 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas.begin_port_drag | 412-419 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas.update_port_drag | 421-424 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas.finish_port_drag | 426-435 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas._node_at | 437-441 | presentation/co4e/co4e_canvas_widget.py | dùng self._scene.items(scene_pt) — cần scene đang sống | chua/khong ro | - |
|
||||
| Co4ECanvas._make_edge | 443-451 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas._add_edge_item | 453-456 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas.delete_edge | 458-463 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas.delete_node | 465-475 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas.delete_selected | 477-481 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas._zoom_by | 484-495 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas.zoom_in | 497-498 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas.zoom_out | 500-501 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas.reset_zoom | 503-505 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas.wheelEvent | 507-519 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas.mousePressEvent | 522-529 | presentation/co4e/co4e_canvas_widget.py | trùng tên với _NodeItem.mousePressEvent nhưng là override của Co4ECanvas (pan chuột giữa) — đừng nhầm khi gộp map | chua/khong ro | - |
|
||||
| Co4ECanvas.mouseMoveEvent | 531-540 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas.mouseReleaseEvent | 542-548 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas.fit_view | 550-558 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas.relayout | 560-578 | presentation/co4e/co4e_canvas_widget.py | phần tính waves/cols (compute_waves, defaultdict) là logic thuần, phần item.setPos() cần scene sống — có thể tách hàm tính toạ độ ra khỏi phần apply nếu muốn test thuần Python | chua/khong ro | - |
|
||||
| Co4ECanvas.relayout_if_vertical | 580-589 | presentation/co4e/co4e_canvas_widget.py | bản thân chỉ ra quyết định thuần dựa trên node.x, việc chạm Qt nằm trong relayout() được gọi | chua/khong ro | - |
|
||||
| Co4ECanvas.add_workflow | 591-610 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas.update_node_status | 612-616 | presentation/co4e/co4e_canvas_widget.py | được co4e_run_control_widget.py gọi khi step chạy/xong/lỗi — điểm nối giữa canvas và run control | chua/khong ro | - |
|
||||
| Co4ECanvas.reset_statuses | 618-621 | presentation/co4e/co4e_canvas_widget.py | được co4e_run_control_widget.py gọi khi bắt đầu run mới | chua/khong ro | - |
|
||||
| Co4ECanvas.refresh_node | 623-626 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas._node_rects | 628-638 | presentation/co4e/co4e_canvas_widget.py | dựng QRectF như kiểu giá trị từ item.pos() — logic hình học thuần | chua/khong ro | - |
|
||||
| Co4ECanvas._reposition_edges | 640-649 | presentation/co4e/co4e_canvas_widget.py | gọi e.update_path (setPath) trên item sống, dùng hàm định tuyến _route từ canvas_geometry.py | chua/khong ro | - |
|
||||
| Co4ECanvas.keyPressEvent | 652-669 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas.dragEnterEvent | 671-675 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas.dragMoveEvent | 677-681 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ECanvas.dropEvent | 683-701 | presentation/co4e/co4e_canvas_widget.py | [GỘP 2 lượt quét trùng ký hiệu] (dòng 683-700, target gốc=presentation/co4e/co4e_canvas_widget.py) cắt ngang lát — cần agent gộp đối chiếu (thân try chưa đóng ở dòng 700, còn tiếp; xử lý JSON từ mimeData trong bộ nhớ, không phải file đĩa nên touches_disk_or_network=false) \|\| (dòng 683-701, target gốc=presentation/co4e/co4e_canvas_widget.py) cắt ngang lát — cần agent gộp đối chiếu. Method dropEvent bắt đầu ở dòng 683 (ngoài khoảng được giao 701-791), chỉ dòng 701 (e.acceptProposedAction()) nằm trong lát này; dòng 702 là dòng trống cuối file. File ui/co4e_canvas.py chỉ có 702 dòng — không có nội dung nào khác trong khoảng 702-791 vì đã hết file. Xử lý drop từ sidebar (kind=='workflow' → add_workflow, ngược lại → add_palette_step) — logic thao tác node/canvas nên khớp nhóm _add_node/_connect_nodes trong bảng plan.md dòng 623 dù plan.md không liệt kê rõ dropEvent. | co | nnect_nodes trong bảng plan.md dòng 623 dù plan.md không liệt kê rõ dropEvent. |
|
||||
| Co4ETab.canvas | 856 | presentation/co4e/co4e_canvas_widget.py | TRẠNG THÁI CHIA SẺ lớn nhất trong file — self.canvas được đọc/ghi ở gần như mọi nhóm chức năng khác (sync_wf_from_canvas, apply_workflow, node selection, run, add_blank_step...). | chua/khong ro | - |
|
||||
| Co4ETab._build_canvas_overlay | 1041-1062 | presentation/co4e/co4e_canvas_widget.py | dựng nút zoom/fit gắn vào self.canvas.add_overlay(bar); các slot gọi thẳng self.canvas.zoom_in/zoom_out/fit_view (thuộc nhóm 3.2.2.21-22 trong plan.md → co4e_canvas_widget.py). Có thể tranh cãi nên giữ ở co4e_tab.py (được gọi từ _build_center) — ghi lại để người soát quyết. | co | - |
|
||||
| Co4ETab.zoom_in_btn | 1054 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab.zoom_out_btn | 1055 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab.fit_btn | 1056 | presentation/co4e/co4e_canvas_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| _skill_names | 61-65 | presentation/co4e/co4e_chat_view.py | gọi skills_mod.list_skills()/builtin_skills() (đọc đĩa); dùng cho autocomplete /skill: trong _ChatInput — cũng liên quan skills_list_panel.py vì cùng nguồn dữ liệu | chua/khong ro | - |
|
||||
| _agent_names | 68-71 | presentation/co4e/co4e_chat_view.py | gọi co4e.list_custom_agents() (đọc đĩa); dùng cho autocomplete /agent: trong _ChatInput — cũng liên quan agent_list_panel.py vì cùng nguồn dữ liệu | chua/khong ro | - |
|
||||
| _directive_token | 122-134 | presentation/co4e/co4e_chat_view.py | logic regex thuần Python, test được không cần Qt | chua/khong ro | - |
|
||||
| _ChatInput | 137-226 | presentation/co4e/co4e_chat_view.py | khai báo 'submit = Signal()' ở dòng 141 là thuộc tính lớp (không phải self.) | chua/khong ro | - |
|
||||
| _ChatInput.__init__ | 143-151 | presentation/co4e/co4e_chat_view.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| _ChatInput._popup | 145 | presentation/co4e/co4e_chat_view.py | QListWidget popup autocomplete | chua/khong ro | - |
|
||||
| _ChatInput._maybe_popup | 153-178 | presentation/co4e/co4e_chat_view.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| _ChatInput._add_row | 180-184 | presentation/co4e/co4e_chat_view.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| _ChatInput._accept | 186-199 | presentation/co4e/co4e_chat_view.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| _ChatInput.focusOutEvent | 201-204 | presentation/co4e/co4e_chat_view.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| _ChatInput.keyPressEvent | 206-226 | presentation/co4e/co4e_chat_view.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab._chat_worker | 236 | presentation/co4e/co4e_chat_view.py | AgentWorker của chat, hiện giữ trên Co4ETab | chua/khong ro | - |
|
||||
| Co4ETab._build_chat | 1064-1127 | presentation/co4e/co4e_chat_view.py | dài ~63 dòng, gộp: header 'Messages' + toggle, chat_stack (QStackedWidget chứa 1 ChatView/flow), input row + usage label + composer + routing toggle. Tạo self._flow_logs (Dict[str, ChatView]) — TRẠNG THÁI CHIA SẺ dùng bởi _ensure_flow_log/_active_log/chat_log/_apply_workflow (self._wf) — điểm dễ vỡ nhất khi tách file chat ra khỏi co4e_tab.py. | chua/khong ro | - |
|
||||
| Co4ETab._chat_widget | 1066 | presentation/co4e/co4e_chat_view.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab._mhdr | 1072 | presentation/co4e/co4e_chat_view.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab.msgs_icon | 1074 | presentation/co4e/co4e_chat_view.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab.msgs_title | 1075 | presentation/co4e/co4e_chat_view.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab.chat_toggle_btn | 1076 | presentation/co4e/co4e_chat_view.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab.chat_stack | 1091 | presentation/co4e/co4e_chat_view.py | chuyển self.center_stack tương tự — dùng chung với self.center_stack (co4e_tab.py). | chua/khong ro | - |
|
||||
| Co4ETab._flow_logs | 1092 | presentation/co4e/co4e_chat_view.py | Dict[str, ChatView] keyed theo workflow id — TRẠNG THÁI CHIA SẺ chính của toàn bộ logic chat theo-flow; đọc/ghi bởi _ensure_flow_log, _active_log, chat_log, _apply_workflow, và các hàm chat khác ngoài phạm vi đọc (>1400). | chua/khong ro | - |
|
||||
| Co4ETab.chat_input_row | 1094 | presentation/co4e/co4e_chat_view.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab._usage_total_lbl | 1099 | presentation/co4e/co4e_chat_view.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab.chat_input | 1105 | presentation/co4e/co4e_chat_view.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab.chat_send_btn | 1108 | presentation/co4e/co4e_chat_view.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab.co4e_routing_toggle | 1113 | presentation/co4e/co4e_chat_view.py | RoutingToggle(self.ctx, 'co4e') — self.ctx là trạng thái chia sẻ của cả Co4ETab (khởi tạo ngoài phạm vi đọc). | chua/khong ro | - |
|
||||
| Co4ETab._co4e_routed_provider | 1114; 1853 | presentation/co4e/co4e_chat_view.py | [GỘP 2 lượt quét trùng ký hiệu] (dòng 1114-1114, target gốc=presentation/co4e/co4e_chat_view.py) override provider cho lượt chat kế tiếp — dùng bởi _apply_co4e_routing (ngoài phạm vi đọc, >1400). \|\| (dòng 1853-1853, target gốc=presentation/co4e/co4e_chat_view.py) gán None lần đầu tại đây (trong _apply_co4e_routing); đọc bằng getattr(self, '_co4e_routed_provider', None) ở _run_chat_turn dòng ~1922 — gợi ý có thể không được init trong __init__. Trạng thái routing theo từng lượt chat, dùng chung giữa chat-view và _start_canvas_run/manager.start. | chua/khong ro | [GỘP 2 lượt quét trùng ký hiệu] (dòng 1114-1114, target gốc=presentation/co4e/co4e_chat_view.py) overr |
|
||||
| Co4ETab._vsplit_sizes | 1121 | presentation/co4e/co4e_chat_view.py | dùng bởi _toggle_messages để restore kích thước splitter — chia sẻ với self._vsplit (co4e_tab.py). | chua/khong ro | - |
|
||||
| Co4ETab._msgs_collapsed | 1122 | presentation/co4e/co4e_chat_view.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab._toggle_messages | 1129-1161 | presentation/co4e/co4e_chat_view.py | thao tác self._vsplit (tạo ở co4e_tab.py/_build_center) — trạng thái chia sẻ giữa co4e_chat_view.py và co4e_tab.py. | chua/khong ro | - |
|
||||
| Co4ETab._ensure_flow_log | 1164-1173 | presentation/co4e/co4e_chat_view.py | đọc/ghi self._flow_logs — trạng thái chia sẻ chính của chat theo-flow. | chua/khong ro | - |
|
||||
| Co4ETab._active_log | 1175-1177 | presentation/co4e/co4e_chat_view.py | đọc self._wf — trạng thái chia sẻ với toàn bộ Co4ETab (canvas, sidebar, save/autosave, run control). | chua/khong ro | - |
|
||||
| Co4ETab.chat_log | 1180-1183 | presentation/co4e/co4e_chat_view.py | @property, chỉ đọc, uỷ nhiệm cho _active_log(). | chua/khong ro | - |
|
||||
| Co4ETab._plan_bubble | 1186-1187 | presentation/co4e/co4e_chat_view.py | @property getter, đọc self._active_log()._co4e_plan_bubble (thuộc tính gắn thêm vào từng instance ChatView). | chua/khong ro | - |
|
||||
| Co4ETab._plan_bubble (setter) | 1189-1191 | presentation/co4e/co4e_chat_view.py | @_plan_bubble.setter, ghi self._active_log()._co4e_plan_bubble. | chua/khong ro | - |
|
||||
| Co4ETab._chat_send | 1820-1846 | presentation/co4e/co4e_chat_view.py | self.chat_input.clear(), self._append_chat — thao tác widget; điều phối /agent /skill directive rồi gọi self._run_chat_turn (network qua provider, không trực tiếp trong hàm này) | chua/khong ro | - |
|
||||
| Co4ETab._apply_co4e_routing | 1848-1883 | presentation/co4e/co4e_chat_view.py | gọi confirm_switch (dialog Qt) khi mode=='manual' và self._append_chat khi có switch — cần widget sống. Gán self._co4e_routed_provider (xem attribute riêng). | chua/khong ro | - |
|
||||
| Co4ETab._extract_agent_directive | 1885-1891 | presentation/co4e/co4e_chat_view.py | regex thuần Python | chua/khong ro | - |
|
||||
| Co4ETab._resolve_agent | 1893-1901 | presentation/co4e/co4e_chat_view.py | tra cứu BUILTIN_AGENTS và co4e.list_custom_agents() (bộ nhớ/đĩa tùy triển khai list_custom_agents, không rõ trong khoảng đọc) — cross-reference với agent_list_panel.py (nguồn danh sách custom agent) | chua/khong ro | - |
|
||||
| Co4ETab._run_chat_turn | 1903-1974 | presentation/co4e/co4e_chat_view.py | 72 dòng, gộp: chuẩn bị prompt, tạo bubble stream, định nghĩa 4 closures nội bộ (job/on_event/done/failed) và khởi worker nền — nên tách các closures ra thành hàm riêng nếu dễ đọc hơn. Closure job() gọi run_cowork(provider,...) — network/AI call thật sự. Đọc/ghi self._chat_worker, self._co4e_routed_provider, self.chat_send_btn, self._wf; set log._co4e_plan_bubble=None (thuộc log là ChatView, không phải self). | chua/khong ro | - |
|
||||
| Co4ETab._run_chat_turn.job | 1917-1946 | presentation/co4e/co4e_chat_view.py | closure nội bộ của _run_chat_turn, chạy trong AgentWorker (thread nền); gọi run_cowork(provider,...) — network/AI call thật; dùng usage_tracker (đọc/ghi trạng thái tích lũy usage) | chua/khong ro | - |
|
||||
| Co4ETab._run_chat_turn.on_event | 1948-1954 | presentation/co4e/co4e_chat_view.py | closure nội bộ, cập nhật assistant.set_markdown và log.scroll_to_bottom (widget) | chua/khong ro | - |
|
||||
| Co4ETab._run_chat_turn.done | 1956-1962 | presentation/co4e/co4e_chat_view.py | closure nội bộ, gán self._chat_worker = None, thao tác widget assistant/log | chua/khong ro | - |
|
||||
| Co4ETab._run_chat_turn.failed | 1964-1967 | presentation/co4e/co4e_chat_view.py | closure nội bộ, gán self._chat_worker = None, gọi self._append_chat lỗi | chua/khong ro | - |
|
||||
| Co4ETab._append_chat | 1976-1991 | presentation/co4e/co4e_chat_view.py | nhận tham số log tùy chọn, mặc định self.chat_log — dùng bởi rất nhiều method ở cả run-control (qua tham số log truyền vào) và chat-view; điểm nối giữa hai nhóm. | chua/khong ro | - |
|
||||
| Co4ETab._fmt_usage | 1994-2001 | presentation/co4e/co4e_chat_view.py | format chuỗi thuần, đọc self.ctx.config.data — không chạm widget | chua/khong ro | - |
|
||||
| Co4ETab._apply_usage | 2003-2020 | presentation/co4e/co4e_chat_view.py | bub.add_usage(...) thao tác widget bubble; ghi self._flow_usage[wf_id] — trạng thái tổng usage theo flow, dùng chung với _refresh_usage_total | chua/khong ro | - |
|
||||
| Co4ETab._refresh_usage_total | 2022-2037 | presentation/co4e/co4e_chat_view.py | self._usage_total_lbl.setText — đọc self._wf, self._flow_usage | chua/khong ro | - |
|
||||
| Co4ETab._append_diff | 2039-2043 | presentation/co4e/co4e_chat_view.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab._append_plan | 2045-2056 | presentation/co4e/co4e_chat_view.py | đọc/ghi log._co4e_plan_bubble (thuộc tính động trên đối tượng log/ChatView, không phải self) | chua/khong ro | - |
|
||||
| _html_escape | 2082-2083 | presentation/co4e/co4e_chat_view.py | hàm module-level (ngoài class Co4ETab), escape HTML thuần, dùng cho hiển thị chat | chua/khong ro | - |
|
||||
| _PLAN_GLYPH | 45-46 | presentation/co4e/co4e_run_control_widget.py | module-level dict glyph trạng thái dùng bởi _fmt_plan | chua/khong ro | - |
|
||||
| _fmt_plan | 49-58 | presentation/co4e/co4e_run_control_widget.py | helper thuần Python dùng bởi _render_plan() | chua/khong ro | - |
|
||||
| Co4ETab.manager | 238 | presentation/co4e/co4e_run_control_widget.py | Co4ERunManager — lõi run control, trạng thái chia sẻ với canvas (node status) và chat view (run_logs) | chua/khong ro | - |
|
||||
| Co4ETab._flow_runs | 243 | presentation/co4e/co4e_run_control_widget.py | map wf_id->run id, chia sẻ với _open_flow/_close_flow_tab (co4e_tab.py) và canvas | chua/khong ro | - |
|
||||
| Co4ETab._run_logs | 244 | presentation/co4e/co4e_run_control_widget.py | map run_id->ChatView, chia sẻ với co4e_chat_view.py | chua/khong ro | - |
|
||||
| Co4ETab._flow_usage | 248 | presentation/co4e/co4e_run_control_widget.py | usage token/cost theo flow, hiển thị ở Messages header | chua/khong ro | - |
|
||||
| Co4ETab._manual_active | 252 | presentation/co4e/co4e_run_control_widget.py | chia sẻ với _close_flow_tab (co4e_tab.py) — set lại self.run_btn khi đóng tab | chua/khong ro | - |
|
||||
| Co4ETab._manual_order | 253 | presentation/co4e/co4e_run_control_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab._manual_idx | 254 | presentation/co4e/co4e_run_control_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab._cur_run_id | 460-473 | presentation/co4e/co4e_run_control_widget.py | logic thuần Python, không gọi Qt trực tiếp | chua/khong ro | - |
|
||||
| Co4ETab._update_run_btn | 480-482 | presentation/co4e/co4e_run_control_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab.runs_more_btn | 586 | presentation/co4e/co4e_run_control_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab.runs_side_list | 594 | presentation/co4e/co4e_run_control_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab._SIDE_RUNS | 605 | presentation/co4e/co4e_run_control_widget.py | class-level constant, không phải self. | chua/khong ro | - |
|
||||
| Co4ETab._refresh_side_runs | 607-619 | presentation/co4e/co4e_run_control_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab._on_side_run_clicked | 621-629 | presentation/co4e/co4e_run_control_widget.py | chạm self.runs_table (định nghĩa ngoài khoảng đọc — thuộc trang Flow Status) | chua/khong ro | - |
|
||||
| Co4ETab.mode_combo | 828 | presentation/co4e/co4e_run_control_widget.py | combo chọn run mode auto/plan/manual — thuộc nhóm _set_run_mode() trong plan.md dù được dựng bên trong _build_center (co4e_tab.py); ranh giới tách file ở đây dễ vỡ. | co | - |
|
||||
| Co4ETab.run_btn | 833 | presentation/co4e/co4e_run_control_widget.py | nút Run — tương tự mode_combo, dựng trong _build_center nhưng thuộc nhóm run control; cũng bị _update_run_btn (<701), _on_run_clicked, _on_mode_changed đọc/ghi text. | chua/khong ro | - |
|
||||
| Co4ETab.runs_btn | 840 | presentation/co4e/co4e_run_control_widget.py | toggle chuyển sang trang Runs (self.center_stack) — chia sẻ state center_stack với co4e_tab.py. | chua/khong ro | - |
|
||||
| Co4ETab._build_runs_page | 873-932 | presentation/co4e/co4e_run_control_widget.py | dựng trang bảng Runs (theo dõi mọi run của mọi flow) — dùng self.manager (Co4ERunManager, sẽ thay bằng co4e_workflow_service) — trạng thái chia sẻ. | chua/khong ro | - |
|
||||
| Co4ETab.runs_back_btn | 882 | presentation/co4e/co4e_run_control_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab.runs_title | 887 | presentation/co4e/co4e_run_control_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab.ws_folder_btn | 892 | presentation/co4e/co4e_run_control_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab.run_stop_btn | 900 | presentation/co4e/co4e_run_control_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab.run_rename_btn | 905 | presentation/co4e/co4e_run_control_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab.run_del_btn | 909 | presentation/co4e/co4e_run_control_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab.run_clear_btn | 913 | presentation/co4e/co4e_run_control_widget.py | clicked gọi self.manager.clear_finished() — trạng thái chia sẻ self.manager. | chua/khong ro | - |
|
||||
| Co4ETab.runs_table | 921 | presentation/co4e/co4e_run_control_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab._current_mode | 1384-1385 | presentation/co4e/co4e_run_control_widget.py | đọc self.mode_combo — khớp nhóm _set_run_mode() dòng 624 plan.md. | co | - |
|
||||
| Co4ETab._on_mode_changed | 1387-1393 | presentation/co4e/co4e_run_control_widget.py | reset self._manual_active/_manual_order/_manual_idx (trạng thái chia sẻ với luồng manual step _manual_step/_manual_run_or_advance, ngoài phạm vi đọc >1400) và gọi self._cur_run_id() (chia sẻ với self.manager). | chua/khong ro | - |
|
||||
| Co4ETab._on_run_clicked | 1395-1400 | presentation/co4e/co4e_run_control_widget.py | cắt ngang lát — cần agent gộp đối chiếu (tiếp tục sau dòng 1400). Dùng self.manager.stop() và self._cur_run_id() — trạng thái chia sẻ với co4e_workflow_service tương lai. Khớp nhóm _run_flow()/_stop_flow() dòng 618 plan.md (dù đó là bản service, đây là UI handler nút Run). | co | - |
|
||||
| Co4ETab.<method truoc dong 1401, ten chua xac dinh - co the la _on_run_clicked> | 1401-1405 | presentation/co4e/co4e_run_control_widget.py | cắt ngang lát — cần agent gộp đối chiếu. Định nghĩa (def ...) nằm ở lát trước dòng 1401 nên tên chính xác không xác định được; đoạn thấy được chỉ dispatch theo self._current_mode() sang _manual_run_or_advance() hoặc _start_canvas_run() — thuộc nhóm Run/mode. | chua/khong ro | (def ...) nằm ở lát trước dòng 1401 nên tên chính xác không xác định được; đoạn thấy được chỉ dispatch theo self._current_mode() sang _manual_run_or |
|
||||
| Co4ETab._start_canvas_run | 1407-1424 | presentation/co4e/co4e_run_control_widget.py | gọi self.manager.start(...) — spawn chạy flow (dẫn tới gọi provider AI ở tầng khác); đọc/ghi self._wf, self._flow_runs, self._flow_runs[wf_id], self._run_logs, self.chat_log — trạng thái chia sẻ giữa run-control và chat-view. self.manager (Co4ERunManager) nên đổi sang application/workflows/co4e_workflow_service.py theo mapping. | chua/khong ro | - |
|
||||
| Co4ETab._run_single | 1426-1431 | presentation/co4e/co4e_run_control_widget.py | wrapper mỏng gọi _start_canvas_run; đọc self._wf.id | chua/khong ro | - |
|
||||
| Co4ETab._run_from | 1433-1437 | presentation/co4e/co4e_run_control_widget.py | wrapper mỏng gọi _start_canvas_run với self._downstream(node_id) | chua/khong ro | - |
|
||||
| Co4ETab._downstream | 1439-1450 | presentation/co4e/co4e_run_control_widget.py | thuật toán BFS thuần Python, chỉ đọc self.canvas.edges() làm input — test được với danh sách edge giả lập, không cần canvas thật | chua/khong ro | - |
|
||||
| Co4ETab._manual_run_or_advance | 1453-1466 | presentation/co4e/co4e_run_control_widget.py | gọi self.canvas.reset_statuses() và self._append_chat (self._append_chat thuộc nhóm chat-view) — cắt ngang giữa run-control và chat-view. Đọc/ghi self._manual_active, self._manual_order, self._manual_idx, self._wf, self._outputs_for(...), self._plan_bubble — trạng thái run thuần túy chia sẻ với _manual_step. | chua/khong ro | - |
|
||||
| Co4ETab._manual_step | 1468-1484 | presentation/co4e/co4e_run_control_widget.py | gọi self.manager.start(...) (network/AI qua provider) và self.run_btn.setText, self._append_chat (chat-view); đọc/ghi self._manual_idx, self._manual_order, self._wf, self._flow_runs, self._run_logs, self.chat_log — trạng thái chia sẻ rộng giữa run-control và chat-view. | chua/khong ro | - |
|
||||
| Co4ETab._topo_order | 1486-1491 | presentation/co4e/co4e_run_control_widget.py | đọc self.canvas.nodes()/edges() làm dữ liệu đầu vào, logic tính toán thuần túy (co4e.compute_waves) — test được với dữ liệu giả | chua/khong ro | - |
|
||||
| Co4ETab._on_manager_event | 1494-1550 | presentation/co4e/co4e_run_control_widget.py | method dài (57 dòng), gộp nhiều việc không liên quan: định tuyến sự kiện chạy theo từng flow (routing run_id -> log), cập nhật trạng thái node trên canvas, hiển thị bubble chat (assistant/diff/plan/tool-failed — thuộc co4e_chat_view.py), xử lý hoàn tất run (run_done/run_error), hiển thị popup thông báo, cập nhật status bar. Nên tách phần hiển thị chat (self._append_chat/_append_diff/_append_plan) sang co4e_chat_view.py, giữ phần routing/flow-completion ở co4e_run_control_widget.py. Đọc/ghi self._flow_runs, self._run_logs, self.chat_log, self.canvas, self._wf, self._outputs_for(...), self._manual_active, self._manual_idx — trạng thái chia sẻ rất rộng, điểm dễ vỡ nhất khi tách file. | chua/khong ro | - |
|
||||
| Co4ETab._notify_run_finished | 1552-1573 | presentation/co4e/co4e_run_control_widget.py | tạo QMessageBox không chặn; khởi tạo lazy self._run_popups (xem attribute riêng) | chua/khong ro | - |
|
||||
| Co4ETab._run_popups | 1560-1561 | presentation/co4e/co4e_run_control_widget.py | khởi tạo lazy (list rỗng) trong _notify_run_finished qua hasattr guard — không init trong __init__; giữ ref các QMessageBox non-blocking khỏi bị GC | chua/khong ro | - |
|
||||
| Co4ETab._refresh_runs | 1575-1616 | presentation/co4e/co4e_run_control_widget.py | render bảng runs_table + gọi self._refresh_side_runs() (sidebar) + cập nhật self.flow_bar.setTabText và self._sections['co4e.runs_tab'] — self._sections và self.flow_bar là trạng thái chia sẻ với co4e_tab.py (container/sidebar). | chua/khong ro | - |
|
||||
| Co4ETab._stop_selected_run | 1618-1624 | presentation/co4e/co4e_run_control_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab._delete_selected_run | 1626-1639 | presentation/co4e/co4e_run_control_widget.py | đọc/ghi self._flow_runs, self._run_logs — trạng thái chia sẻ với _on_manager_event/_start_canvas_run | chua/khong ro | - |
|
||||
| Co4ETab._runs_context_menu | 1641-1655 | presentation/co4e/co4e_run_control_widget.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab._open_run_output_folder | 1704-1715 | presentation/co4e/co4e_run_control_widget.py | path.mkdir + open_location (spawn process); dùng bởi _runs_context_menu 'Open output' | chua/khong ro | - |
|
||||
| Co4ETab._rename_selected_run | 1717-1748 | presentation/co4e/co4e_run_control_widget.py | gọi co4e.save_workflow(wf) (ghi đĩa); đọc/ghi self._flows (list flow của sidebar/tab-bar), self.flow_bar, self.name_edit, self._wf — chạm nhiều trạng thái chia sẻ với co4e_tab.py container (tab bar + name edit thuộc header, không rõ nằm ở panel nào) — rủi ro vỡ cao khi tách. | chua/khong ro | - |
|
||||
| Co4ETab._run_selected_in_background | 1750-1760 | presentation/co4e/co4e_run_control_widget.py | gọi self.manager.start(...) (network/AI) và self._refresh_side_runs() (widget) | chua/khong ro | - |
|
||||
| Co4ETab._wf_by_id | 1762-1770 | presentation/co4e/co4e_run_control_widget.py | gọi co4e.get_workflow(wf_id) (đọc đĩa) và self._sync_wf_from_canvas() (không nằm trong khoảng đọc) — đọc self._wf; dùng chung bởi _rerun_run_item/_open_run_from_table (Runs tab) và có thể cả co4e_tab.py container | chua/khong ro | - |
|
||||
| Co4ETab._rerun_run_item | 1772-1783 | presentation/co4e/co4e_run_control_widget.py | gọi self.manager.start(...) (network/AI) | chua/khong ro | - |
|
||||
| Co4ETab._open_run_from_table | 1785-1804 | presentation/co4e/co4e_run_control_widget.py | gọi self._open_flow(wf) (không nằm trong khoảng đọc, theo plan.md thuộc co4e_tab.py) và self.canvas.update_node_status — nối Runs tab với việc mở flow trên canvas, điểm khớp nối giữa run-control và container/canvas. | co | - |
|
||||
| _qcolor | 2086-2088 | presentation/co4e/co4e_run_control_widget.py | hàm module-level (ngoài class Co4ETab), tạo QColor từ hex string — dùng kiểu giá trị QColor, không cần QApplication sống; dùng trong _refresh_runs để tô màu status | chua/khong ro | - |
|
||||
| _EqualTabBar | 74-94 | presentation/co4e/co4e_tab.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| _EqualTabBar._GAP | 80 | presentation/co4e/co4e_tab.py | class-level constant, không phải self. | chua/khong ro | - |
|
||||
| _EqualTabBar.tabSizeHint | 82-90 | presentation/co4e/co4e_tab.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| _EqualTabBar.resizeEvent | 92-94 | presentation/co4e/co4e_tab.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab | 229-700 | presentation/co4e/co4e_tab.py | Lớp kéo dài quá dòng 700 (cắt ngang lát) — chỉ ghi nhận phần 229-700; 'status_message = Signal(str)' dòng 230 là thuộc tính lớp, không phải self. | chua/khong ro | - |
|
||||
| Co4ETab.__init__ | 232-304 | presentation/co4e/co4e_tab.py | method 73 dòng, gộp: khởi state run/flow, dựng splitter 3 cột (sidebar/center/config), wiring canvas & config panel, gọi _reload_sidebar (đọc đĩa qua co4e.list_workflows) và _open_flow; chạm rất nhiều thuộc tính chia sẻ: self._wf, self._flows, self.manager, self._flow_runs, self._run_logs, self._flow_outputs, self._flow_usage, self.config, self.canvas (gán ở _build_center ngoài khoảng đọc này) | chua/khong ro | - |
|
||||
| Co4ETab.ctx | 234 | presentation/co4e/co4e_tab.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab._wf | 235 | presentation/co4e/co4e_tab.py | trạng thái chia sẻ — đọc/ghi bởi canvas, run control, chat view | chua/khong ro | - |
|
||||
| Co4ETab._flows | 257 | presentation/co4e/co4e_tab.py | danh sách flow đang mở dạng tab — trạng thái chia sẻ nhạy cảm (nêu rõ trong hướng dẫn đề bài) | chua/khong ro | - |
|
||||
| Co4ETab._active_flow_idx | 258 | presentation/co4e/co4e_tab.py | chỉ số tab đang active — chia sẻ giữa _open_flow, _on_flow_tab_changed, _reflect_active_run | chua/khong ro | - |
|
||||
| Co4ETab._split | 261 | presentation/co4e/co4e_tab.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab._config_collapsed | 274 | presentation/co4e/co4e_tab.py | trạng thái cho _toggle_config (định nghĩa dòng 989, ngoài khoảng đọc); plan.md xếp _toggle_config ở co4e_tab.py container | co | - |
|
||||
| Co4ETab._config_expanded_w | 275; 995 | presentation/co4e/co4e_tab.py | [GỘP 2 lượt quét trùng ký hiệu] (dòng 275-275, target gốc=presentation/co4e/co4e_tab.py) cùng lý do với _config_collapsed \|\| (dòng 995-995, target gốc=presentation/co4e/co4e_tab.py) gán trong nhánh collapse của _toggle_config; có thể đã khởi tạo trong __init__ (ngoài phạm vi đọc). | chua/khong ro | [GỘP 2 lượt quét trùng ký hiệu] (dòng 275-275, target gốc=presentation/co4e/co4e_tab.py) cùng lý do vớ |
|
||||
| Co4ETab._narrow_guard | 280 | presentation/co4e/co4e_tab.py | gắn với _apply_narrow_layout (dòng 977, ngoài khoảng đọc) | chua/khong ro | - |
|
||||
| Co4ETab._open_flow | 307-338 | presentation/co4e/co4e_tab.py | khớp plan.md: _open_flow() -> co4e_tab.py, gọi canvas; chạm self._flows, self.flow_bar (định nghĩa ngoài khoảng đọc), self.canvas | co | - |
|
||||
| Co4ETab._close_other_flows | 340-355 | presentation/co4e/co4e_tab.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab._show_runs | 357-367 | presentation/co4e/co4e_tab.py | chuyển đổi center_stack giữa flow editor và Runs table — liên quan Flow Status (run control) | chua/khong ro | - |
|
||||
| Co4ETab._on_flow_tab_changed | 369-385 | presentation/co4e/co4e_tab.py | chạm self.center_stack (định nghĩa ngoài khoảng đọc, ở _build_center) | chua/khong ro | - |
|
||||
| Co4ETab._sync_runs_toggle | 387-394 | presentation/co4e/co4e_tab.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab._add_tab_close_button | 396-406 | presentation/co4e/co4e_tab.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab._close_flow_tab_button | 408-412 | presentation/co4e/co4e_tab.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab._close_flow_tab | 414-442 | presentation/co4e/co4e_tab.py | chạm self._flow_runs/_run_logs/_manual_active/self.run_btn (chia sẻ với run control widget) | chua/khong ro | - |
|
||||
| Co4ETab._sync_active_flow_tab_text | 444-447 | presentation/co4e/co4e_tab.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab._build_sidebar | 485-603 | presentation/co4e/co4e_tab.py | >80 dòng (119 dòng) — gộp dựng 4 section (Workflows/Agents/Skills/Runs) + wiring nhiều nút bấm; nên tách theo section: Workflows giữ ở container, Agents nên chuyển agent_list_panel.py, Skills đã tách (chỉ còn wiring), Runs nên chuyển co4e_run_control_widget.py | chua/khong ro | - |
|
||||
| Co4ETab._sections | 493 | presentation/co4e/co4e_tab.py | trạng thái chia sẻ (nêu rõ trong hướng dẫn đề bài) — dùng bởi _fold_section, _sync_section_arrow | chua/khong ro | - |
|
||||
| Co4ETab.sidebar | 494 | presentation/co4e/co4e_tab.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab.side_split | 498 | presentation/co4e/co4e_tab.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| _Col | 503-511 | presentation/co4e/co4e_tab.py | adapter nội bộ định nghĩa bên trong _build_sidebar, chỉ dùng tại chỗ | chua/khong ro | - |
|
||||
| _Col.__init__ | 506-507 | presentation/co4e/co4e_tab.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| _Col.addWidget | 509-511 | presentation/co4e/co4e_tab.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab.wf_new_btn | 516 | presentation/co4e/co4e_tab.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab.wf_list | 527 | presentation/co4e/co4e_tab.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab.wf_edit_btn | 534 | presentation/co4e/co4e_tab.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab.wf_dup_btn | 535 | presentation/co4e/co4e_tab.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab.wf_del_btn | 536 | presentation/co4e/co4e_tab.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab.wf_runbg_btn | 543 | presentation/co4e/co4e_tab.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab._skills_panel | 575 | presentation/co4e/co4e_tab.py | instantiate SkillsListPanel đã tách; Co4ETab giữ wiring theo comment trong code (dòng 571-574) | chua/khong ro | - |
|
||||
| Co4ETab.sk_manage_btn | 576 | presentation/co4e/co4e_tab.py | cùng lý do với _skills_panel | chua/khong ro | - |
|
||||
| Co4ETab.skill_list | 578 | presentation/co4e/co4e_tab.py | cùng lý do với _skills_panel | chua/khong ro | - |
|
||||
| Co4ETab._section | 631-663 | presentation/co4e/co4e_tab.py | chạm self._sections (chia sẻ) — helper dựng section sidebar dùng chung | chua/khong ro | - |
|
||||
| Co4ETab._fold_section | 665-677 | presentation/co4e/co4e_tab.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab._sync_section_arrow | 679-681 | presentation/co4e/co4e_tab.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab._icon_btn | 683-687 | presentation/co4e/co4e_tab.py | helper dùng chung tạo nút icon, dùng bởi cả section Workflows và Agents | chua/khong ro | - |
|
||||
| Co4ETab._reload_sidebar | 689-722 | presentation/co4e/co4e_tab.py | [GỘP 2 lượt quét trùng ký hiệu] (dòng 689-700, target gốc=presentation/co4e/co4e_tab.py) cắt ngang lát — cần agent gộp đối chiếu (method tiếp tục sau dòng 700); gọi co4e.list_workflows() (đọc đĩa) rồi dựng QListWidgetItem trực tiếp trong cùng hàm — trộn data-fetch và UI, nên tách phần đọc dữ liệu sang co4e_workflow_service.py \|\| (dòng 689-722, target gốc=unsure) cắt ngang lát — cần agent gộp đối chiếu (bắt đầu trước dòng 701). Gộp refresh cả agent_list và skill_list trong cùng 1 hàm — nên tách thành _refresh_agents_list (agent_list_panel.py) và _refresh_skills_list (skills_list_panel.py) như plan.md yêu cầu; gọi co4e.list_custom_agents() (đĩa) và skills_mod.skill_prefix_for (đĩa). | co | [GỘP 2 lượt quét trùng ký hiệu] (dòng 689-700, target gốc=presentation/co4e/co4e_tab.py) cắt ngang lát |
|
||||
| Co4ETab._build_center | 731-871 | presentation/co4e/co4e_tab.py | dài ~140 dòng, làm nhiều việc không liên quan: dựng flow tab bar (ẩn, không hiển thị cho user), dựng runs-page stack, toolbar flow (name_edit/save/save_tpl/mode_combo/run_btn/runs_btn), tạo canvas, gọi _build_canvas_overlay, tạo chat widget, splitter dọc self._vsplit. Nên tách nhỏ. Đọc self._wf.name (dòng 814) và tạo self.center_stack — TRẠNG THÁI CHIA SẺ dùng bởi _show_runs/_apply_workflow (ngoài phạm vi đọc). Khớp phần '_build_canvas()' trong bảng plan.md dòng 615. | co | - |
|
||||
| Co4ETab.flow_bar | 738 | presentation/co4e/co4e_tab.py | QTabBar bị ẩn (setVisible False dòng 802), chỉ dùng làm index nội bộ ánh xạ flow↔canvas — trạng thái chia sẻ với _on_flow_tab_changed/_close_flow_tab (định nghĩa <701, ngoài phạm vi đọc). | chua/khong ro | - |
|
||||
| Co4ETab.flow_add_btn | 764 | presentation/co4e/co4e_tab.py | ẩn (setVisible False), không hiển thị cho user hiện tại. | chua/khong ro | - |
|
||||
| Co4ETab.flow_scroll | 781 | presentation/co4e/co4e_tab.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab.center_stack | 805 | presentation/co4e/co4e_tab.py | TRẠNG THÁI CHIA SẺ — chuyển đổi giữa trang Runs (co4e_run_control_widget) và trang flow editor; dùng bởi _show_runs (ngoài phạm vi đọc) và _apply_workflow. | chua/khong ro | - |
|
||||
| Co4ETab.name_edit | 814 | presentation/co4e/co4e_tab.py | khởi tạo từ self._wf.name — trạng thái chia sẻ; cũng bị _on_name_changed/_new_workflow đọc/ghi. | chua/khong ro | - |
|
||||
| Co4ETab.add_step_btn | 819 | presentation/co4e/co4e_tab.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab.save_btn | 822 | presentation/co4e/co4e_tab.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab.save_tpl_btn | 826 | presentation/co4e/co4e_tab.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab._vsplit | 863 | presentation/co4e/co4e_tab.py | splitter dọc canvas/chat — dùng chung với _toggle_messages (co4e_chat_view.py thao tác self._vsplit.setSizes) — trạng thái chia sẻ giữa co4e_tab.py và co4e_chat_view.py. | chua/khong ro | - |
|
||||
| Co4ETab._NARROW | 971 | presentation/co4e/co4e_tab.py | hằng class-level (không phải self.), ngưỡng chiều rộng dùng bởi _apply_narrow_layout. | chua/khong ro | - |
|
||||
| Co4ETab.showEvent | 973-975 | presentation/co4e/co4e_tab.py | Qt override, gọi self._narrow_guard.attach() (đối tượng khởi tạo ngoài phạm vi đọc, <701). [ANOMALY: cùng tên method định nghĩa 2 lần ở 2 dòng khác nhau trong cùng class -- có khả năng là lỗi nguồn (định nghĩa sau đè định nghĩa trước), không phải do quét trùng; giữ 2 dòng riêng, cần người quyết định định nghĩa nào còn hiệu lực] | chua/khong ro | ợng khởi tạo ngoài phạm vi đọc, <701). [ANOMALY: cùng tên method định nghĩa 2 lần ở 2 dòng khác nhau trong cùng class -- có khả |
|
||||
| Co4ETab._apply_narrow_layout | 977-987 | presentation/co4e/co4e_tab.py | so self._config_collapsed rồi gọi self._toggle_config() — trạng thái chia sẻ với node_property_panel wrap logic. | chua/khong ro | - |
|
||||
| Co4ETab._toggle_config | 989-1032 | presentation/co4e/co4e_tab.py | dài 44 dòng, gộp: ẩn/hiện self.config (widget thuộc node_property_panel.py), đổi icon, tính lại self._split.setSizes, gọi _refresh_min_width — thao tác trực tiếp self.config/self.config_container (node_property_panel.py) và self._split (co4e_tab.py) cùng lúc — ranh giới tách file dễ vỡ nhất ở đoạn này. Ghi self._config_expanded_w. | chua/khong ro | - |
|
||||
| Co4ETab._refresh_min_width | 1034-1039 | presentation/co4e/co4e_tab.py | thao tác trực tiếp self._split (QSplitter) và self.config_container — chia sẻ với _build_center/_wrap_config. | chua/khong ro | - |
|
||||
| Co4ETab._apply_workflow | 1194-1207 | presentation/co4e/co4e_tab.py | hàm điều phối trung tâm khi mở 1 flow: gán self._wf (TRẠNG THÁI CHIA SẺ dùng khắp mọi nhóm chức năng — canvas, chat_stack/_flow_logs, config panel, run button, usage total). Đây là điểm nối chính giữa các file sau khi tách — không nên tách nhỏ hơn nếu không rất cẩn thận. | chua/khong ro | - |
|
||||
| Co4ETab._new_workflow | 1209-1217 | presentation/co4e/co4e_tab.py | gọi self._open_flow(...) — khớp dòng plan.md '_open_flow() -> co4e_tab.py → gọi canvas'. | co | - |
|
||||
| Co4ETab._selected_wf | 1219-1225 | presentation/co4e/co4e_tab.py | đọc self.wf_list (sidebar, dựng ở _build_sidebar <701) rồi gọi co4e.get_workflow (đĩa). | chua/khong ro | - |
|
||||
| Co4ETab._load_selected_workflow | 1227-1230 | presentation/co4e/co4e_tab.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab._edit_selected_workflow | 1232-1237 | presentation/co4e/co4e_tab.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab._wf_context_menu | 1248-1271 | presentation/co4e/co4e_tab.py | dựng QMenu cho sidebar flows, điều phối gọi _edit_selected_workflow/_rename_workflow/_duplicate_selected_workflow/_run_selected_in_background/_delete_selected_workflow (một số nằm ngoài phạm vi đọc, dòng >1400). | chua/khong ro | - |
|
||||
| Co4ETab._sync_wf_from_canvas | 1299-1302 | presentation/co4e/co4e_tab.py | đọc self.canvas.nodes()/edges() và ghi self._wf.nodes/edges/name — cầu nối giữa co4e_canvas_widget.py và trạng thái self._wf chia sẻ; cân nhắc đặt cùng canvas nếu muốn canvas tự chịu trách nhiệm export dữ liệu. | chua/khong ro | - |
|
||||
| Co4ETab._on_name_changed | 1316-1318 | presentation/co4e/co4e_tab.py | ghi self._wf.name (trạng thái chia sẻ) rồi gọi self._sync_active_flow_tab_text() (định nghĩa dòng 444, ngoài phạm vi đọc). | chua/khong ro | - |
|
||||
| Co4ETab._add_blank_step | 1320-1322 | presentation/co4e/co4e_tab.py | nút 'Add' trên toolbar uỷ nhiệm sang self.canvas.add_palette_step — liên quan nhóm _add_node() trong plan.md (co4e_canvas_widget.py) nhưng bản thân handler chỉ là cầu nối từ toolbar. | co | - |
|
||||
| Co4ETab.set_project | 1658-1674 | presentation/co4e/co4e_tab.py | gọi load_project(project_id) (đọc đĩa) và self.manager.set_output_root/set_current_project (self.manager nên là co4e_workflow_service). Gán self._project_id, self._project_dir — không chắc là lần gán đầu tiên (có thể đã init ở __init__ ngoài khoảng đọc). Không khớp rõ nhóm nào trong danh sách đích cho trước — xếp tạm vào co4e_tab.py vì đây là API binding cấp container gọi từ ngoài. | chua/khong ro | ể đã init ở __init__ ngoài khoảng đọc). Không khớp rõ nhóm nào trong danh sách đích cho trước — xếp tạm vào co4e_tab.py vì đây là |
|
||||
| Co4ETab._flow_output_root | 1676-1686 | presentation/co4e/co4e_tab.py | helper dùng chung bởi cả chat (_out_dir) và run-control (_open_workspace_folder, _open_run_output_folder) — trạng thái/logic cắt ngang nhiều nhóm; đọc self._project_dir, self.ctx.config | chua/khong ro | - |
|
||||
| Co4ETab._refresh_ws_folder_btn | 1688-1693 | presentation/co4e/co4e_tab.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab._open_workspace_folder | 1695-1702 | presentation/co4e/co4e_tab.py | root.mkdir + open_location (mở file explorer hệ điều hành — spawn process) | chua/khong ro | - |
|
||||
| Co4ETab.showEvent | 1806-1809 | presentation/co4e/co4e_tab.py | override Qt lifecycle method của chính QWidget container nên phải ở lại co4e_tab.py; gọi self._refresh_runs() (thuộc co4e_run_control_widget.py) — điểm nối giữa container và run-control widget. [ANOMALY: cùng tên method định nghĩa 2 lần ở 2 dòng khác nhau trong cùng class -- có khả năng là lỗi nguồn (định nghĩa sau đè định nghĩa trước), không phải do quét trùng; giữ 2 dòng riêng, cần người quyết định định nghĩa nào còn hiệu lực] | chua/khong ro | giữa container và run-control widget. [ANOMALY: cùng tên method định nghĩa 2 lần ở 2 dòng khác nhau trong cùng class -- có khả |
|
||||
| Co4ETab._out_dir | 1811-1817 | presentation/co4e/co4e_tab.py | d.mkdir(parents=True, exist_ok=True) — ghi đĩa; dùng chung bởi _run_chat_turn (chat) và tiềm năng bởi run-control; đọc self._wf.name, self._flow_output_root() | chua/khong ro | - |
|
||||
| Co4ETab._retranslate | 2059-2079 | presentation/co4e/co4e_tab.py | cập nhật text i18n cho rất nhiều widget thuộc nhiều nhóm khác nhau (sidebar buttons, runs_table, run control buttons) và gọi self._reload_sidebar()/self._refresh_runs() — thuộc container vì bao trùm toàn tab, dù có thể tách nhỏ theo từng panel sau này. | chua/khong ro | - |
|
||||
| _SectionHeader | 30-52 | presentation/co4e/node_property_panel.py | Widget nội bộ (header có thể click, thu/mở section) chỉ dùng bởi StepConfigPanel; không có trong bảng plan.md/function_list.md, xếp cùng chỗ với StepConfigPanel vì mô tả node_property_panel.py là 'Bọc StepConfigPanel' | co | section) chỉ dùng bởi StepConfigPanel; không có trong bảng plan.md/function_list.md, xếp cùng chỗ với StepConfigPanel vì mô tả node_property_panel |
|
||||
| _SectionHeader.clicked | 36 | presentation/co4e/node_property_panel.py | Signal Qt khai báo ở cấp class, không phải self.<tên> gán trong __init__ | chua/khong ro | - |
|
||||
| _SectionHeader.mousePressEvent | 38-41 | presentation/co4e/node_property_panel.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| _SectionHeader.showEvent | 43-52 | presentation/co4e/node_property_panel.py | recompute fontMetrics khi label thực sự hiển thị — cần widget đang sống, không test được nếu không có QApplication | chua/khong ro | - |
|
||||
| _add_section | 55-130 | presentation/co4e/node_property_panel.py | Hàm dựng UI khối section thu gọn/mở rộng (header + body animate) dùng riêng cho StepConfigPanel; chứa 2 closure nội bộ _on_finished và _toggle. Không có trong bảng cũ. | co | closure nội bộ _on_finished và _toggle. Không có trong bảng cũ. |
|
||||
| _add_section._on_finished | 103-112 | presentation/co4e/node_property_panel.py | closure lồng bên trong _add_section, không phải hàm top-level — chỉ tồn tại khi _add_section chạy | chua/khong ro | - |
|
||||
| _add_section._toggle | 114-127 | presentation/co4e/node_property_panel.py | closure lồng bên trong _add_section, gắn vào header.clicked | chua/khong ro | - |
|
||||
| StepConfigPanel | 133-528 | presentation/co4e/node_property_panel.py | cắt ngang lát — cần agent gộp đối chiếu (lớp còn tiếp tục sau dòng 528, chỉ đọc được 1-528). Bảng plan.md/function_list.md không liệt kê StepConfigPanel trực tiếp (chỉ có _build_config_panel() -> co4e_tab.py container); xếp theo mô tả node_property_panel.py 'Bọc StepConfigPanel, nối chọn node sang panel thuộc tính' trong danh sách đích được giao cho task này — người quyết cuối nên xác nhận lại. | co | - |
|
||||
| StepConfigPanel.changed | 134 | presentation/co4e/node_property_panel.py | Signal Qt: bất kỳ field nào đổi -> canvas repaint node + autosave; đây là điểm nối trạng thái chia sẻ với canvas/co4e_workflow_service, cần giữ tên/signature khi tách file | chua/khong ro | - |
|
||||
| StepConfigPanel.run_node | 135 | presentation/co4e/node_property_panel.py | Signal Qt 'chạy step này' — nối sang co4e_run_control_widget.py hoặc co4e_workflow_service.py | chua/khong ro | - |
|
||||
| StepConfigPanel.run_from | 136 | presentation/co4e/node_property_panel.py | Signal Qt 'chạy từ bước này' — nối sang co4e_run_control_widget.py | chua/khong ro | - |
|
||||
| StepConfigPanel.delete_node | 137 | presentation/co4e/node_property_panel.py | Signal Qt xoá step — nối sang co4e_canvas_widget.py để xoá node trên canvas | chua/khong ro | - |
|
||||
| StepConfigPanel.__init__ | 139-313 | presentation/co4e/node_property_panel.py | >80 dòng (174 dòng) — gộp nhiều việc không liên quan: dựng section Cơ bản, section Model&Quyền, section Skills&Tệp, section Sub-agents (ẩn/hiện theo is_parallel), và dựng hàng nút footer Run/Run-from/Delete, cộng thêm cơ chế 'outer.addStretch(1)' vá lỗi layout. Nên tách thành các hàm _build_basic_section(), _build_model_section(), _build_skills_section(), _build_subagent_section(), _build_footer() riêng khi tách file. | chua/khong ro | - |
|
||||
| StepConfigPanel.ctx | 141 | presentation/co4e/node_property_panel.py | context được truyền từ ngoài vào, dùng cho _ai_draft/_load_models (gọi AI provider) — trạng thái chia sẻ với co4e_tab.py | chua/khong ro | - |
|
||||
| StepConfigPanel._step | 142 | presentation/co4e/node_property_panel.py | Step đang chỉnh sửa — trạng thái chia sẻ giữa load_step()/_on_edit()/mọi hành động subagent+attachment; do canvas gán vào qua load_step() | chua/khong ro | - |
|
||||
| StepConfigPanel._node_id | 143 | presentation/co4e/node_property_panel.py | id node đang chọn — dùng để emit run_node/run_from/delete_node; là cầu nối canvas <-> property panel | chua/khong ro | - |
|
||||
| StepConfigPanel._loading | 144 | presentation/co4e/node_property_panel.py | cờ chặn _on_edit() chạy lại trong lúc load_step() đang set giá trị field | chua/khong ro | - |
|
||||
| StepConfigPanel.label_edit | 159 | presentation/co4e/node_property_panel.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| StepConfigPanel.role_edit | 163 | presentation/co4e/node_property_panel.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| StepConfigPanel.icon_edit | 170 | presentation/co4e/node_property_panel.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| StepConfigPanel.instructions_edit | 175 | presentation/co4e/node_property_panel.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| StepConfigPanel.gen_btn | 178 | presentation/co4e/node_property_panel.py | nút 'AI draft' — enable chỉ khi có ctx | chua/khong ro | - |
|
||||
| StepConfigPanel.context_edit | 192 | presentation/co4e/node_property_panel.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| StepConfigPanel.model_combo | 201 | presentation/co4e/node_property_panel.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| StepConfigPanel.load_models_btn | 204 | presentation/co4e/node_property_panel.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| StepConfigPanel.perm_combo | 214 | presentation/co4e/node_property_panel.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| StepConfigPanel.verify_chk | 221 | presentation/co4e/node_property_panel.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| StepConfigPanel.rounds_spin | 223 | presentation/co4e/node_property_panel.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| StepConfigPanel.skills_list | 236 | presentation/co4e/node_property_panel.py | checklist skill của registry, gán checked theo step.skills | chua/khong ro | - |
|
||||
| StepConfigPanel.attach_list | 242 | presentation/co4e/node_property_panel.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| StepConfigPanel.attach_add_btn | 244 | presentation/co4e/node_property_panel.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| StepConfigPanel.attach_del_btn | 247 | presentation/co4e/node_property_panel.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| StepConfigPanel._parallel_card | 263 | presentation/co4e/node_property_panel.py | card cả section Sub-agents; load_step() ẩn/hiện toàn bộ card này theo step.is_parallel | chua/khong ro | - |
|
||||
| StepConfigPanel.sub_list | 264 | presentation/co4e/node_property_panel.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| StepConfigPanel.sub_add_btn | 267 | presentation/co4e/node_property_panel.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab.config | 268 | presentation/co4e/node_property_panel.py | StepConfigPanel + wiring changed/run_node/run_from/delete_node (dòng 268-273) — khớp vai trò mô tả cho node_property_panel.py | chua/khong ro | - |
|
||||
| StepConfigPanel.sub_del_btn | 270 | presentation/co4e/node_property_panel.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| StepConfigPanel.run_btn | 283 | presentation/co4e/node_property_panel.py | click emit run_node(self._node_id) | chua/khong ro | - |
|
||||
| StepConfigPanel.run_from_btn | 287 | presentation/co4e/node_property_panel.py | click emit run_from(self._node_id) | chua/khong ro | - |
|
||||
| StepConfigPanel.del_btn | 290 | presentation/co4e/node_property_panel.py | click emit delete_node(self._node_id) | chua/khong ro | - |
|
||||
| StepConfigPanel.load_step | 316-354 | presentation/co4e/node_property_panel.py | Điểm nối chính giữa canvas (khi chọn node) và panel thuộc tính — nhận (node_id, step, skill_names) từ ngoài rồi ghi self._step/self._node_id; đây là API mà co4e_canvas_widget.py hoặc co4e_tab.py sẽ gọi khi chọn node | chua/khong ro | - |
|
||||
| StepConfigPanel.clear_step | 356-359 | presentation/co4e/node_property_panel.py | gọi khi bỏ chọn node — reset self._step/self._node_id | chua/khong ro | - |
|
||||
| StepConfigPanel._on_edit | 362-378 | presentation/co4e/node_property_panel.py | ghi ngược giá trị field UI vào self._step rồi emit changed() — canvas repaint + autosave phụ thuộc signal này, đổi tên/behavior ở đây ảnh hưởng cả canvas lẫn service lưu flow | chua/khong ro | - |
|
||||
| StepConfigPanel._available_agent_names | 380-390 | presentation/co4e/node_property_panel.py | staticmethod; gọi core.co4e.list_custom_agents() đọc file JSON trong AGENTS_DIR trên đĩa — logic thuần Python nhưng có I/O, có thể tách ra application layer nếu cần test không đụng đĩa | chua/khong ro | - |
|
||||
| StepConfigPanel._add_subagent | 392-408 | presentation/co4e/node_property_panel.py | dùng QInputDialog để chọn/nhập tên agent song song | chua/khong ro | - |
|
||||
| StepConfigPanel._edit_subagent | 410-428 | presentation/co4e/node_property_panel.py | double-click 1 dòng sub-agent để chọn lại agent khác | chua/khong ro | - |
|
||||
| StepConfigPanel._del_subagent | 430-437 | presentation/co4e/node_property_panel.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| StepConfigPanel._add_attachment | 439-453 | presentation/co4e/node_property_panel.py | QFileDialog chỉ chọn đường dẫn hiển thị tên file, không tự đọc nội dung ở đây (nội dung được đọc lúc chạy step, ở nơi khác) | chua/khong ro | - |
|
||||
| StepConfigPanel._del_attachment | 455-462 | presentation/co4e/node_property_panel.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| StepConfigPanel._ai_draft | 464-498 | presentation/co4e/node_property_panel.py | gọi generate_agent_prompt(ctx.build_active_provider(), ...) qua AgentWorker — gọi network tới AI provider, chạy nền rồi cập nhật UI ở callback done(); nếu tách sang service, phần gọi AI nên chuyển xuống application layer, phần còn lại (QInputDialog + set text) ở lại đây | chua/khong ro | - |
|
||||
| StepConfigPanel._draft_worker | 497 | presentation/co4e/node_property_panel.py | giữ tham chiếu AgentWorker (QThread-like) để không bị GC giữa lúc job async đang chạy | chua/khong ro | - |
|
||||
| StepConfigPanel._load_models | 500-528 | presentation/co4e/node_property_panel.py | gọi preview_ai.fetch_live_models(ctx) qua AgentWorker — network call tới provider để lấy danh sách model; cắt ngang lát — cần agent gộp đối chiếu vì dòng cuối trùng đúng biên đọc được giao (528), chưa chắc thân method đã hết ở đây | chua/khong ro | - |
|
||||
| StepConfigPanel._model_worker | 525 | presentation/co4e/node_property_panel.py | giữ tham chiếu AgentWorker của _load_models để không bị GC | chua/khong ro | - |
|
||||
| Co4ETab._wrap_config | 934-964 | presentation/co4e/node_property_panel.py | bọc self.config (StepConfigPanel) với header expand/collapse — khớp mô tả node_property_panel.py trong prompt. | chua/khong ro | - |
|
||||
| Co4ETab.config_toggle_btn | 946 | presentation/co4e/node_property_panel.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab.config_title | 951 | presentation/co4e/node_property_panel.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab._cfg_vlayout | 957 | presentation/co4e/node_property_panel.py | dùng lại trong _toggle_config (co4e_tab.py) — trạng thái chia sẻ. | chua/khong ro | - |
|
||||
| Co4ETab._cfg_top_spacer | 961 | presentation/co4e/node_property_panel.py | QSpacerItem — kiểu giá trị layout, không cần QApplication đang sống. | chua/khong ro | - |
|
||||
| Co4ETab._cfg_bot_spacer | 962 | presentation/co4e/node_property_panel.py | (không có ghi chú) | chua/khong ro | - |
|
||||
| Co4ETab.config_container | 963 | presentation/co4e/node_property_panel.py | dùng lại bởi _toggle_config (co4e_tab.py) — trạng thái chia sẻ giữa node_property_panel.py và co4e_tab.py. | chua/khong ro | - |
|
||||
| Co4ETab._on_node_selected | 1325-1331 | presentation/co4e/node_property_panel.py | khớp mô tả 'nối chọn node sang panel thuộc tính' trong prompt. Đọc self.canvas.nodes() và self.config, toggle self._config_collapsed (trạng thái chia sẻ với _toggle_config ở co4e_tab.py). | chua/khong ro | - |
|
||||
| Co4ETab._on_config_changed | 1333-1336 | presentation/co4e/node_property_panel.py | gọi self.canvas.refresh_node cho từng node rồi self._autosave() — cầu nối property panel ↔ canvas ↔ service lưu đĩa. | chua/khong ro | - |
|
||||
| Co4ETab._manage_skills | 1369-1373 | presentation/co4e/skills_list_panel.py | mở SkillsDialog rồi self._reload_sidebar() — không có tên tương ứng trực tiếp trong bảng plan.md, tự xếp theo mô tả skills_list_panel.py. | co | - |
|
||||
| _PaletteList | 97-119 | unsure | list kéo-thả dùng chung cho wf_list và agent_list, phát payload CO4E_MIME hiểu bởi canvas — không rõ nên đặt ở co4e_tab.py (nơi dùng) hay co4e_canvas_widget.py (định nghĩa giao thức CO4E_MIME) | chua/khong ro | - |
|
||||
| _PaletteList.__init__ | 102-106 | unsure | cùng lý do với class _PaletteList | chua/khong ro | - |
|
||||
| _PaletteList._payload_role | 104 | unsure | cùng lý do với class _PaletteList | chua/khong ro | - |
|
||||
| _PaletteList.startDrag | 108-119 | unsure | cùng lý do với class _PaletteList; dùng CO4E_MIME từ co4e_canvas | chua/khong ro | - |
|
||||
| Co4ETab._project_id | 249 | unsure | project Workspace đang chọn, ảnh hưởng đường dẫn output flow — không chắc thuộc container hay run control | chua/khong ro | - |
|
||||
| Co4ETab._project_dir | 250 | unsure | cùng lý do với _project_id | chua/khong ro | - |
|
||||
| Co4ETab._reflect_active_run | 449-457 | unsure | cầu nối giữa self.manager (run control) và self.canvas (canvas widget) — không chắc nên đặt file nào | chua/khong ro | - |
|
||||
| Co4ETab._palette_item | 724-728 | unsure | staticmethod helper dùng chung để tạo QListWidgetItem cho cả agent_list, skill_list, và cả node palette sequential/parallel (thấy dùng ở dòng 701-704) — không rõ nên đặt ở agent_list_panel.py, skills_list_panel.py hay co4e_canvas_widget.py. | chua/khong ro | - |
|
||||
|
||||
## Chỗ thấy khác tài liệu — cần người quyết
|
||||
|
||||
Không có symbol nào có note bắt đầu bằng `khac tai lieu:` trong dữ liệu 367 symbol đầu vào. Mục này để trống theo đúng dữ liệu quét được — không tự bịa thêm mục.
|
||||
@@ -0,0 +1,39 @@
|
||||
# Danh Mục & Kế Hoạch Cô Lập Mã Nguồn Dormant / Dead Code (Dormant Code Catalog)
|
||||
|
||||
* **Tài liệu**: `docs/architecture/dormant-code.md`
|
||||
* **Thuộc EPIC**: `R01: Architecture Foundation & Characterization`
|
||||
* **Team phụ trách**: 🔵 **Team Duy (Tech Lead)**
|
||||
|
||||
---
|
||||
|
||||
## 1. Mục Đích & Nguyên Tắc Quản Trị
|
||||
|
||||
Trong quá trình phát triển nhanh, một số module, hàm hoặc script đã trở thành mã nguồn không hoạt động (**dormant**), mã nguồn thử nghiệm cũ (**legacy prototypes**), hoặc mã nguồn không còn được sử dụng (**dead code**).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> ### 🛡️ NGUYÊN TẮC CÔ LẬP MÃ NGUỒN CŨ:
|
||||
> 1. **Tuyệt đối không import vào các tầng mới**: Các tầng `domain/`, `application/`, `infrastructure/` mới được xây dựng **cấm tuyệt đối import bất kỳ module dormant nào**.
|
||||
> 2. **Không xóa vội vàng khi chưa có test bảo vệ**: Giữ nguyên mã nguồn cũ trong giai đoạn tái cấu trúc R01–R08; chỉ dọn dẹp hoặc xóa sau khi bộ kiểm thử khói E2E (EPIC R10) chạy pass 100%.
|
||||
> 3. **Phân loại rõ ràng trạng thái**: Mỗi module dormant phải được gắn nhãn (DEPRECATED / ISOLATED / PENDING_DELETION).
|
||||
|
||||
---
|
||||
|
||||
## 2. Bảng Danh Mục Mã Nguồn Dormant / Dead Code Đã Rà Soát
|
||||
|
||||
| STT | File / Module / Ký Hiệu | Trạng Thái Hiện Tại | Lý Do Phân Loại & Phân Tích Kỹ Thuật | Kế Hoạch Xử Lý & Thời Điểm Gỡ Bỏ |
|
||||
| :---: | :--- | :---: | :--- | :--- |
|
||||
| **1** | `requirements (cloud copy).txt` | `PENDING_DELETION` | File sao chép dự phòng tạm thời trong quá khứ, không được tham chiếu bởi bất kỳ quy trình setup nào. | Gỡ bỏ trong EPIC R10 (Packaging & Clean-up). |
|
||||
| **2** | `preview-desktop` | `ISOLATED` | Script shell rỗng/phác thảo cho môi trường dev container cũ. | Cô lập, không liên kết vào build workflow. |
|
||||
| **3** | `scripts/bootstrap_gitea_repo.py` | `ISOLATED` | Script tiện ích bootstrap kho lưu trữ Gitea nội bộ; không thuộc runtime ứng dụng chính. | Di chuyển vào `docs/gitea/` làm tài liệu tham khảo ops. |
|
||||
| **4** | Hàm routing sao chép tại `ui/chat_panel.py#L638` | `DEPRECATED` | Đoạn code logic chọn model lặp lại từ `core/routing/` nằm trực tiếp trong UI widget. | Thay thế hoàn toàn bằng `RoutingApplicationService` trong EPIC R03. |
|
||||
| **5** | Biến toàn cục `state.py::active_project_id` | `DEPRECATED` | Biến global mutable gây race condition khi chạy background task song song. | Thay thế bằng `WorkspaceSession` trong EPIC R06. |
|
||||
| **6** | Các hàm xử lý UI đồng bộ trong `core/tools.py` | `DEPRECATED` | `core/tools.py` chứa mã monolithic vừa xử lý file vừa gọi dialog xác thực trực tiếp. | Phân rã thành `file_tools.py`, `command_tools.py` và `ToolPolicyGateway` trong EPIC R05. |
|
||||
|
||||
---
|
||||
|
||||
## 3. Quy Trình Cô Lập & Kiểm Soát
|
||||
|
||||
1. **Kiểm tra tự động qua AST Guard**:
|
||||
- Bộ script `scripts/check_imports.py` tự động quét để đảm bảo không có bất kỳ import mới nào trỏ tới các thành phần đã đánh dấu deprecated.
|
||||
2. **Kế hoạch dọn dẹp cuối cùng (Release Phase - 31/08/2026)**:
|
||||
- Sau khi hoàn thành EPIC R10 và pass toàn bộ bài test E2E (`tests/e2e/test_smoke.py`), các file đánh dấu `PENDING_DELETION` sẽ được gỡ bỏ khỏi nhánh `main`.
|
||||
@@ -0,0 +1,159 @@
|
||||
# Mô hình chính sách an toàn — CoworkLocal
|
||||
|
||||
R09-T01 · Team Gamma · viết 22/08/2026
|
||||
|
||||
Tài liệu này mô tả **hệ thống đang chạy**, không phải hệ thống mong muốn. Mọi
|
||||
khẳng định đều chỉ tới file và dòng cụ thể để đối chiếu được.
|
||||
|
||||
---
|
||||
|
||||
## 1. Câu hỏi quan trọng nhất: đây có phải rào chắn an ninh không
|
||||
|
||||
**Không.** `core/agent_security.py` nói thẳng ngay ở đầu file:
|
||||
|
||||
> *"this is a business productivity tool, not a hard security boundary"*
|
||||
|
||||
Điều đó quyết định mọi thứ còn lại. Cụ thể: **mọi tầng dùng AI đều mở khi
|
||||
hỏng** (`allowed=True` khi không gọi được validator, `core/agent_security.py:150`).
|
||||
Mạng chập chờn hay gateway trục trặc thì agent vẫn chạy, không bị khoá cứng.
|
||||
|
||||
Đánh đổi có chủ đích: chọn *dùng được* thay vì *chặn tuyệt đối*. Ai đọc tài
|
||||
liệu này để đánh giá rủi ro cần hiểu đúng điều đó — đây là lớp giảm tai nạn,
|
||||
không phải lớp chống kẻ tấn công có chủ đích.
|
||||
|
||||
---
|
||||
|
||||
## 2. Hai loại quy tắc, đừng lẫn
|
||||
|
||||
| | Quy tắc xác định | Quy tắc do AI phán |
|
||||
|---|---|---|
|
||||
| Cách hoạt động | So khớp mẫu cố định | Hỏi một model |
|
||||
| Kết quả | Luôn giống nhau | Có thể khác nhau giữa hai lần |
|
||||
| Khi hỏng | Vẫn chạy | **Mở** (cho qua) |
|
||||
| Tắt được không | Không — luôn bật | Có, từng tầng một |
|
||||
| Ở đâu | Bộ phân loại mẫu chặn + sandbox | 3 tầng validate |
|
||||
|
||||
Câu ở `core/agent_security.py:250` nói rõ ranh giới:
|
||||
|
||||
> *"always-on block-pattern classifier + sandbox still apply regardless"*
|
||||
|
||||
Nghĩa là **tắt hết ba tầng AI thì vẫn còn hai lớp xác định**. Đây là điểm dễ
|
||||
hiểu nhầm nhất khi đọc màn Cài đặt: mấy công tắc ở đó **chỉ tắt phần AI**.
|
||||
|
||||
---
|
||||
|
||||
## 3. Ba tầng AI
|
||||
|
||||
Bật/tắt độc lập trong `agent_security` của `config.json`.
|
||||
|
||||
| Tầng | Kiểm cái gì | Khoá cấu hình | Khi nào chạy |
|
||||
|---|---|---|---|
|
||||
| Prompt | Yêu cầu của chính người dùng | `validate_prompt` | Trước khi agent làm gì |
|
||||
| Attachment | Văn bản trích ra từ tệp đính kèm | `validate_attachments` | Trước khi vào ngữ cảnh model |
|
||||
| Command | `run_command` / `install_package` | `validate_commands` | Trước khi thực thi |
|
||||
|
||||
Cả ba đọc chung một bộ luật: file cục bộ `core/security_rules.py` cộng thêm
|
||||
tài liệu quản trị viên đặt trên OneDrive (nếu có cấu hình). Riêng agent Code
|
||||
dùng bộ luật khác — `RULEforCode.md` thay vì `RULEBASE.md`.
|
||||
|
||||
Công tắc tổng `agent_security.enabled` tắt cả ba.
|
||||
|
||||
---
|
||||
|
||||
## 4. Chuyện gì xảy ra khi bị chặn
|
||||
|
||||
Theo đúng thứ tự trong `core/agent_security.py:266-273`:
|
||||
|
||||
1. Hiện thông báo trong khung chat — người dùng thấy ngay, kèm lý do
|
||||
2. Ghi `audit_log.record("security_block", …)` — vào nhật ký kiểm toán
|
||||
3. `notify_admin(...)` — gửi email quản trị viên
|
||||
4. Ném `SecurityBlocked` — dừng lượt chạy
|
||||
|
||||
Ba bước đầu **không được phép ném lỗi**. `audit_log.record()` có ghi rõ trong
|
||||
docstring: *"never raises — audit logging must never break a chat turn"*. Ghi
|
||||
nhật ký hỏng không được kéo theo cả phiên làm việc.
|
||||
|
||||
---
|
||||
|
||||
## 5. Hỏi người dùng: trạng thái thứ ba
|
||||
|
||||
Ngoài cho/chặn còn một trạng thái nữa mà hệ thống hiện tại **có nhưng chưa gọi
|
||||
tên**: hỏi người dùng.
|
||||
|
||||
`ui/chat_panel.py:1312` kiểm `ctx.project_confirm_commands()` rồi bật
|
||||
`PermissionDialog`. Đó là một quyết định chính sách thật, nhưng nằm rải ở tầng
|
||||
giao diện chứ không phải một kết quả chính thức.
|
||||
|
||||
`domain/security/tool_policy.py` (đề xuất, chờ Team Hoa xác nhận) gộp lại
|
||||
thành ba trạng thái:
|
||||
|
||||
| | Nghĩa |
|
||||
|---|---|
|
||||
| `ALLOW` | Chạy |
|
||||
| `DENY` | Không chạy, có lý do |
|
||||
| `ASK` | Hỏi người dùng đã |
|
||||
|
||||
**`ASK` không phải là `allowed`.** Coi ASK như ALLOW nghĩa là tool chạy trước
|
||||
khi có ai đồng ý — bẫy dễ mắc nhất, đã có test riêng chặn.
|
||||
|
||||
Cổng chính sách **không tự bật hộp thoại**. Nó chỉ trả lời; hỏi ai và hỏi thế
|
||||
nào là việc của tầng giao diện. Nhờ vậy Co4E chạy nền mới dùng chung cổng được
|
||||
với Cowork chạy tương tác — Co4E không hỏi được thì đổi `ASK` thành `DENY`.
|
||||
|
||||
---
|
||||
|
||||
## 6. Bí mật
|
||||
|
||||
Từ 21/08 (R02-T05), API key **không còn nằm trong `config.json`**:
|
||||
|
||||
* Lưu trong kho của hệ điều hành qua `KeyringAdapter` — Windows Credential
|
||||
Manager, macOS Keychain, Linux Secret Service
|
||||
* `provider_conf()` đọc từ kho rồi ghép vào dict trả về, nên chỗ gọi không
|
||||
đổi (đường A, `GammaTeam_decisions.md`)
|
||||
* File cũ tự chuyển ở lần mở đầu tiên, có sao lưu trước khi chuyển
|
||||
|
||||
Máy không có kho bí mật (Linux headless, CI) thì **không chuyển** — thà để
|
||||
khoá trong file còn hơn xoá đi rồi người dùng mất khoá.
|
||||
|
||||
Kiểm bằng `python scripts/audit_security.py`, chạy tự động trong CI.
|
||||
|
||||
---
|
||||
|
||||
## 7. Sandbox
|
||||
|
||||
`core/sandbox_manager.py` chạy lệnh trong môi trường hạn chế. Luôn bật, không
|
||||
tắt được, không phụ thuộc công tắc AI nào.
|
||||
|
||||
Năng lực khác nhau theo hệ điều hành — ma trận đầy đủ sẽ nằm ở
|
||||
`infrastructure/sandbox/sandbox_capabilities.py` (R09-T06, Hiệp phụ trách).
|
||||
Chỗ này cập nhật khi task đó xong.
|
||||
|
||||
---
|
||||
|
||||
## 8. Những chỗ đã biết là yếu
|
||||
|
||||
Ghi ra để người sau khỏi tưởng đã kín:
|
||||
|
||||
1. **Mở khi hỏng.** Gateway chết là ba tầng AI cho qua hết. Có chủ đích, nhưng
|
||||
nghĩa là không chống được kẻ tấn công biết cách làm validator ngừng trả lời.
|
||||
2. **Bí mật vẫn đi trong bộ nhớ.** Đường A ghép khoá vào dict `provider_conf()`
|
||||
trả về, nên khoá vẫn có thể lọt vào log gỡ lỗi hay ảnh chụp màn hình. Đường
|
||||
B (bỏ hẳn khỏi dict) đã ghi vào nợ kỹ thuật.
|
||||
3. **Bộ luật lấy từ OneDrive không ký số.** Ai sửa được tài liệu đó là sửa được
|
||||
luật.
|
||||
4. **`ASK` chưa được nối vào Co4E.** Co4E chạy nền, chưa có đường hỏi người
|
||||
dùng — hiện phải chọn giữa cho qua hết hoặc chặn hết.
|
||||
|
||||
---
|
||||
|
||||
## Đối chiếu nhanh
|
||||
|
||||
| Nội dung | Nguồn |
|
||||
|---|---|
|
||||
| Ba tầng AI, mở khi hỏng | `core/agent_security.py:1-25` |
|
||||
| Phân loại mẫu + sandbox luôn bật | `core/agent_security.py:250` |
|
||||
| Thứ tự khi bị chặn | `core/agent_security.py:266-273` |
|
||||
| Nhật ký không được ném lỗi | `core/audit_log.py:46` |
|
||||
| Hỏi người dùng | `ui/chat_panel.py:1312` |
|
||||
| Ba trạng thái chính sách | `domain/security/tool_policy.py` |
|
||||
| Bí mật | `infrastructure/secrets/keyring_adapter.py` |
|
||||
@@ -0,0 +1,768 @@
|
||||
<!doctype html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Phân Việc Refactor Team Gamma</title>
|
||||
</head>
|
||||
<body>
|
||||
<style>
|
||||
:root {
|
||||
--ground: #FAF9FC; --surface: #FFFFFF; --surface-2: #F3F0F8;
|
||||
--ink: #191325; --muted: #665E7C; --line: #E4DEEE;
|
||||
--accent: #6D3A9E;
|
||||
--lead: #6D3A9E; --m1: #14707F; --m2: #A65418;
|
||||
--warn: #A81F1A; --ok: #1B6B40;
|
||||
--lead-wash: #F1E9F9; --m1-wash: #E2F1F3; --m2-wash: #F8EDE2;
|
||||
}
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-theme="light"]) {
|
||||
--ground: #121020; --surface: #1B1830; --surface-2: #241F3D;
|
||||
--ink: #EFEBF7; --muted: #A69EBD; --line: #2F2848;
|
||||
--accent: #C08CF0;
|
||||
--lead: #C08CF0; --m1: #56C6D8; --m2: #E29A56;
|
||||
--warn: #F08A84; --ok: #6FD69C;
|
||||
--lead-wash: #2A1F42; --m1-wash: #133038; --m2-wash: #38270F;
|
||||
}
|
||||
}
|
||||
:root[data-theme="dark"] {
|
||||
--ground: #121020; --surface: #1B1830; --surface-2: #241F3D;
|
||||
--ink: #EFEBF7; --muted: #A69EBD; --line: #2F2848;
|
||||
--accent: #C08CF0;
|
||||
--lead: #C08CF0; --m1: #56C6D8; --m2: #E29A56;
|
||||
--warn: #F08A84; --ok: #6FD69C;
|
||||
--lead-wash: #2A1F42; --m1-wash: #133038; --m2-wash: #38270F;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0; background: var(--ground); color: var(--ink);
|
||||
font-family: "Segoe UI", -apple-system, system-ui, "Helvetica Neue", sans-serif;
|
||||
font-size: 15px; line-height: 1.62; -webkit-font-smoothing: antialiased;
|
||||
}
|
||||
.wrap { max-width: 1120px; margin: 0 auto; padding: 0 28px 96px; }
|
||||
code, .mono, td.day, th.day, .num {
|
||||
font-family: Consolas, "Cascadia Mono", "SF Mono", ui-monospace, monospace;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
header.top { border-bottom: 2px solid var(--ink); padding: 56px 0 22px; margin-bottom: 34px; }
|
||||
.eyebrow { font-size: 12px; letter-spacing: .16em; text-transform: uppercase;
|
||||
color: var(--accent); font-weight: 700; margin: 0 0 14px; }
|
||||
h1 { font-size: clamp(30px, 4.4vw, 44px); line-height: 1.1; margin: 0 0 16px;
|
||||
font-weight: 700; letter-spacing: -.02em; text-wrap: balance; }
|
||||
.lede { font-size: 17px; color: var(--muted); margin: 0; max-width: 64ch; }
|
||||
.alias { margin: 18px 0 0; padding: 12px 16px; max-width: 74ch;
|
||||
background: var(--surface-2); border-left: 3px solid var(--accent);
|
||||
border-radius: 3px; font-size: 14px; color: var(--muted); }
|
||||
.alias b { color: var(--ink); }
|
||||
.facts { display: flex; flex-wrap: wrap; gap: 28px; margin-top: 26px;
|
||||
padding-top: 20px; border-top: 1px solid var(--line); }
|
||||
.fact .k { font-size: 11px; letter-spacing: .13em; text-transform: uppercase;
|
||||
color: var(--muted); display: block; margin-bottom: 3px; }
|
||||
.fact .v { font-size: 15px; font-weight: 600; }
|
||||
|
||||
h2 { font-size: 23px; margin: 52px 0 6px; letter-spacing: -.01em; font-weight: 700; text-wrap: balance; }
|
||||
h2 + .sub { color: var(--muted); margin: 0 0 22px; max-width: 70ch; }
|
||||
h3 { font-size: 17px; margin: 30px 0 10px; font-weight: 700; }
|
||||
|
||||
/* gate = việc phải xong trước khi chia nhánh */
|
||||
.gatebox { background: var(--surface); border: 1px solid var(--line);
|
||||
border-left: 4px solid var(--warn); border-radius: 3px; padding: 4px 26px 22px; }
|
||||
.gatebox h2 { margin-top: 22px; }
|
||||
|
||||
.steps { list-style: none; counter-reset: s; padding: 0; margin: 0; }
|
||||
.steps > li { counter-increment: s; position: relative; padding: 14px 0 14px 46px;
|
||||
border-bottom: 1px solid var(--line); }
|
||||
.steps > li:last-child { border-bottom: none; }
|
||||
.steps > li::before {
|
||||
content: counter(s); position: absolute; left: 0; top: 14px;
|
||||
width: 26px; height: 26px; border-radius: 50%; background: var(--accent);
|
||||
color: #fff; font-size: 13px; font-weight: 700; display: flex;
|
||||
align-items: center; justify-content: center;
|
||||
font-family: Consolas, ui-monospace, monospace;
|
||||
}
|
||||
.steps b { display: block; margin-bottom: 2px; }
|
||||
.steps small { color: var(--muted); font-size: 13.5px; display: block; }
|
||||
.est { float: right; font-size: 12px; color: var(--muted); font-weight: 600;
|
||||
font-family: Consolas, ui-monospace, monospace; }
|
||||
|
||||
.cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
|
||||
@media (max-width: 940px) { .cards { grid-template-columns: 1fr; } }
|
||||
.card { background: var(--surface); border: 1px solid var(--line);
|
||||
border-top: 3px solid var(--c); border-radius: 3px; padding: 20px;
|
||||
display: flex; flex-direction: column; }
|
||||
.card.lead { --c: var(--lead); --w: var(--lead-wash); }
|
||||
.card.one { --c: var(--m1); --w: var(--m1-wash); }
|
||||
.card.two { --c: var(--m2); --w: var(--m2-wash); }
|
||||
.card .tag { font-size: 11px; letter-spacing: .13em; text-transform: uppercase;
|
||||
font-weight: 700; color: var(--c); margin-bottom: 6px; }
|
||||
.card h3 { margin: 0 0 4px; font-size: 18px; }
|
||||
.card .who { font-size: 13px; color: var(--muted); margin-bottom: 14px; }
|
||||
.card .branch { font-size: 12.5px; background: var(--w); color: var(--c);
|
||||
padding: 5px 9px; border-radius: 3px; display: inline-block;
|
||||
margin-bottom: 16px; word-break: break-all; font-weight: 600; }
|
||||
.card h4 { font-size: 11px; letter-spacing: .12em; text-transform: uppercase;
|
||||
color: var(--muted); margin: 16px 0 7px; font-weight: 700; }
|
||||
.card ul { margin: 0; padding-left: 17px; font-size: 14px; }
|
||||
.card li { margin-bottom: 6px; }
|
||||
.tid { font-size: 12px; font-weight: 700; color: var(--c);
|
||||
font-family: Consolas, ui-monospace, monospace; }
|
||||
.paths { list-style: none; padding: 0; margin: 0; font-size: 12.5px; }
|
||||
.paths li { padding: 3px 0; border-bottom: 1px dotted var(--line);
|
||||
font-family: Consolas, ui-monospace, monospace; color: var(--muted); word-break: break-all; }
|
||||
.paths li:last-child { border-bottom: none; }
|
||||
.weight { margin-top: auto; padding-top: 16px; font-size: 12.5px; color: var(--muted); }
|
||||
.weight b { color: var(--ink); font-size: 15px; }
|
||||
|
||||
.scroll { overflow-x: auto; border: 1px solid var(--line); border-radius: 3px; }
|
||||
table { border-collapse: collapse; width: 100%; font-size: 13.5px; background: var(--surface); }
|
||||
th, td { text-align: left; padding: 11px 14px; border-bottom: 1px solid var(--line); vertical-align: top; }
|
||||
thead th { background: var(--surface-2); font-size: 11px; letter-spacing: .1em;
|
||||
text-transform: uppercase; color: var(--muted); font-weight: 700; white-space: nowrap; }
|
||||
tbody tr:last-child td { border-bottom: none; }
|
||||
td.day, th.day { white-space: nowrap; font-weight: 700; font-size: 13px; }
|
||||
td.cl { border-left: 3px solid var(--lead); }
|
||||
td.c1 { border-left: 3px solid var(--m1); }
|
||||
td.c2 { border-left: 3px solid var(--m2); }
|
||||
tr.mark td { background: var(--surface-2); font-weight: 600; }
|
||||
td small { color: var(--muted); display: block; font-size: 12.5px; }
|
||||
.pill { display: inline-block; font-size: 11px; font-weight: 700; padding: 2px 7px;
|
||||
border-radius: 2px; letter-spacing: .04em; white-space: nowrap; }
|
||||
.pill.cp { background: var(--m1-wash); color: var(--m1); }
|
||||
.pill.gate { background: var(--m2-wash); color: var(--m2); }
|
||||
.pill.ship { background: var(--lead-wash); color: var(--lead); }
|
||||
|
||||
.rules { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; }
|
||||
@media (max-width: 760px) { .rules { grid-template-columns: 1fr; } }
|
||||
.rule { background: var(--surface); border: 1px solid var(--line);
|
||||
border-radius: 3px; padding: 18px 20px; border-left: 3px solid var(--c, var(--line)); }
|
||||
.rule.hard { --c: var(--warn); }
|
||||
.rule.soft { --c: var(--ok); }
|
||||
.rule h3 { margin: 0 0 8px; font-size: 15px; }
|
||||
.rule p { margin: 0; font-size: 14px; color: var(--muted); }
|
||||
.rule code { color: var(--ink); }
|
||||
|
||||
|
||||
/* tóm tắt: đọc 30 giây là nắm được, trước khi vào chi tiết */
|
||||
.tldr {
|
||||
display: grid; grid-template-columns: 1.35fr 1fr; gap: 0;
|
||||
border: 1px solid var(--line); border-radius: 3px; overflow: hidden;
|
||||
margin-bottom: 8px; background: var(--surface);
|
||||
}
|
||||
@media (max-width: 820px) { .tldr { grid-template-columns: 1fr; } }
|
||||
.tldr > div { padding: 20px 24px; }
|
||||
.tldr .right { background: var(--surface-2); border-left: 1px solid var(--line); }
|
||||
@media (max-width: 820px) { .tldr .right { border-left: none; border-top: 1px solid var(--line); } }
|
||||
.tldr .cap {
|
||||
font-size: 11px; letter-spacing: .14em; text-transform: uppercase;
|
||||
color: var(--muted); font-weight: 700; margin: 0 0 12px;
|
||||
}
|
||||
.flow { list-style: none; padding: 0; margin: 0; font-size: 14px; }
|
||||
.flow li { padding: 7px 0; border-bottom: 1px dotted var(--line); display: flex; gap: 10px; }
|
||||
.flow li:last-child { border-bottom: none; }
|
||||
.flow .b {
|
||||
flex: 0 0 auto; font-size: 11.5px; font-weight: 700; padding: 1px 7px; border-radius: 2px;
|
||||
background: var(--w2); color: var(--c2); height: fit-content; margin-top: 2px;
|
||||
font-family: Consolas, ui-monospace, monospace;
|
||||
}
|
||||
.flow li.f0 { --c2: var(--warn); --w2: var(--surface-2); }
|
||||
.flow li.f1 { --c2: var(--lead); --w2: var(--lead-wash); }
|
||||
.flow li.f2 { --c2: var(--m1); --w2: var(--m1-wash); }
|
||||
.flow li.f3 { --c2: var(--m2); --w2: var(--m2-wash); }
|
||||
.flow .t { flex: 1; }
|
||||
.flow .t b { display: block; }
|
||||
.flow .t small { color: var(--muted); font-size: 12.5px; }
|
||||
.must { margin: 0; padding-left: 18px; font-size: 14px; }
|
||||
.must li { margin-bottom: 8px; }
|
||||
.must li:last-child { margin-bottom: 0; }
|
||||
.must b { color: var(--ink); }
|
||||
|
||||
/* input / output từng người */
|
||||
.io { display: grid; gap: 18px; }
|
||||
.iorow { background: var(--surface); border: 1px solid var(--line);
|
||||
border-left: 3px solid var(--c); border-radius: 3px; overflow: hidden; }
|
||||
.iorow.n1 { --c: var(--lead); --w: var(--lead-wash); }
|
||||
.iorow.n2 { --c: var(--m1); --w: var(--m1-wash); }
|
||||
.iorow.n3 { --c: var(--m2); --w: var(--m2-wash); }
|
||||
.iohead { padding: 14px 20px; background: var(--w); display: flex;
|
||||
align-items: baseline; gap: 12px; flex-wrap: wrap; }
|
||||
.iohead b { color: var(--c); font-size: 15px; }
|
||||
.iohead span { color: var(--muted); font-size: 13px; }
|
||||
.iogrid { display: grid; grid-template-columns: 1fr 1fr; }
|
||||
@media (max-width: 860px) { .iogrid { grid-template-columns: 1fr; } }
|
||||
.iogrid > div { padding: 16px 20px; }
|
||||
.iogrid > div + div { border-left: 1px solid var(--line); }
|
||||
@media (max-width: 860px) {
|
||||
.iogrid > div + div { border-left: none; border-top: 1px solid var(--line); }
|
||||
}
|
||||
.iocap { font-size: 11px; letter-spacing: .13em; text-transform: uppercase;
|
||||
color: var(--muted); font-weight: 700; margin: 0 0 10px; }
|
||||
.iolist { list-style: none; margin: 0; padding: 0; font-size: 13.5px; }
|
||||
.iolist li { padding: 5px 0; border-bottom: 1px dotted var(--line); }
|
||||
.iolist li:last-child { border-bottom: none; }
|
||||
.iolist code { font-size: 12.5px; }
|
||||
.frm { display: inline-block; font-size: 11px; font-weight: 700; padding: 1px 6px;
|
||||
border-radius: 2px; background: var(--surface-2); color: var(--muted);
|
||||
margin-right: 6px; font-family: Consolas, ui-monospace, monospace; }
|
||||
.frm.done { background: var(--m1-wash); color: var(--m1); }
|
||||
.frm.risk { background: var(--m2-wash); color: var(--m2); }
|
||||
|
||||
footer { margin-top: 64px; padding-top: 20px; border-top: 1px solid var(--line);
|
||||
font-size: 13px; color: var(--muted); }
|
||||
</style>
|
||||
|
||||
<div class="wrap">
|
||||
|
||||
<header class="top">
|
||||
<p class="eyebrow">Team Gamma · Automation, Workflows & Governance</p>
|
||||
<h1>Một nhánh chung, ba làn không đụng nhau</h1>
|
||||
<p class="lede">
|
||||
Toàn bộ phần việc refactor 10 ngày của Team Gamma — Nam, Hiệp, Lâm. Cả ba đẩy chung vào <code>gamma/refactor</code>. Nam làm thêm một mục chung —
|
||||
khung kiến trúc, hợp đồng dữ liệu, cổng kiểm duyệt — nằm ngoài ba nhánh; xong mục đó thì
|
||||
ba người vào ba nhánh tính năng ngang nhau, không ai phải sửa chung file với ai.
|
||||
</p>
|
||||
<p class="alias">
|
||||
Ba tài liệu refactor gọi team này là <b>“Team Nam”</b> (theo tên lead). Cùng một team, cùng
|
||||
phạm vi R02 · R08 · R09 · R07-T06. Nhánh của team dùng tiền tố <code>gamma/</code>; ba tài liệu refactor viết
|
||||
<code>nam/workflow-governance-*</code> theo tên lead — cùng một thứ.
|
||||
</p>
|
||||
<div class="facts">
|
||||
<div class="fact"><span class="k">Thời hạn</span><span class="v mono">21/08 → 31/08</span></div>
|
||||
<div class="fact"><span class="k">Người</span><span class="v mono">Nam · Hiệp · Lâm</span></div>
|
||||
<div class="fact"><span class="k">Nhánh</span><span class="v mono">gamma/refactor</span></div>
|
||||
<div class="fact"><span class="k">Code phải bóc</span><span class="v mono">~6.500 dòng</span></div>
|
||||
<div class="fact"><span class="k">Cổng phải qua</span><span class="v mono">CASAN Check 1</span></div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
|
||||
<section class="tldr">
|
||||
<div>
|
||||
<p class="cap">Tóm tắt · thứ tự làm</p>
|
||||
<ul class="flow">
|
||||
<li class="f0">
|
||||
<span class="b">CHUNG</span>
|
||||
<span class="t"><b>Nam làm trước, nửa ngày</b>
|
||||
<small>Dựng khung 5 thư mục (đang là 0 file) · interface + fake cho Config/Secrets ·
|
||||
chốt <code>api_key</code> và báo Team Duy · script CASAN Check 1 · đưa 3 check vào CI ·
|
||||
quyết số phận 24 checker UI. Merge xong mới chia nhánh.</small></span>
|
||||
</li>
|
||||
<li class="f1">
|
||||
<span class="b">N1</span>
|
||||
<span class="t"><b>N1 — Nam · Cấu hình, Bí mật, Vỏ ứng dụng</b>
|
||||
<small>R02 (6 task) · settings 4 widget · bootstrap + MainWindow · policy doc.
|
||||
Giữ luôn <code>app.py</code>, <code>config.py</code>, <code>theme.py</code>,
|
||||
<code>i18n.py</code>. ~2.700 dòng.</small></span>
|
||||
</li>
|
||||
<li class="f2">
|
||||
<span class="b">N2</span>
|
||||
<span class="t"><b>N2 — Hiệp · Giám sát</b>
|
||||
<small>7 tab Monitoring · CanonicalAuditLogger · MonitoringQueryService ·
|
||||
2 vòng lặp import · ma trận Sandbox. ~2.650 dòng.</small></span>
|
||||
</li>
|
||||
<li class="f3">
|
||||
<span class="b">N3</span>
|
||||
<span class="t"><b>N3 — Lâm · Co4E Studio</b>
|
||||
<small>Co4EWorkflowService · tách <code>co4e_tab.py</code> + <code>co4e_canvas.py</code>
|
||||
thành 5 phần. ~2.880 dòng, file to nhất team.</small></span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="right">
|
||||
<p class="cap">Ba điều bắt buộc</p>
|
||||
<ol class="must">
|
||||
<li><b>Không chạm file dùng chung.</b> Cần thêm chuỗi hay màu thì nhắn nhóm trưởng, đừng tự sửa.</li>
|
||||
<li><b>Nộp factory, không tự lắp vào <code>app.py</code>.</b> N1 lắp trong <code>bootstrap.py</code> ngày 28/08.</li>
|
||||
<li><b>Bị chặn thì dùng fake, báo ngay trong ngày.</b> Không ngồi đợi ai.</li>
|
||||
</ol>
|
||||
<p class="cap" style="margin-top:20px">Nghiệm thu</p>
|
||||
<p style="margin:0;font-size:14px;color:var(--muted)">
|
||||
Trên <code>gamma/refactor</code>: không file nào được sửa bởi hai người khác nhau.
|
||||
Có là quy ước <b style="color:var(--ink)">số 1</b> đang bị vi phạm.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="gatebox">
|
||||
<h2>Mục chung — Nam làm, xong hai người kia mới bắt đầu</h2>
|
||||
<p class="sub">
|
||||
Sáu việc dưới đây không thuộc làn nào — chúng là thứ cả ba người cùng đụng
|
||||
vào. Nam làm một lần và đẩy lên <code>gamma/refactor</code>, rồi hai người kia
|
||||
mới bắt đầu. Ước tính nửa ngày.
|
||||
</p>
|
||||
<ol class="steps">
|
||||
<li>
|
||||
<span class="est">~30 phút</span>
|
||||
<b>Dựng khung thư mục</b>
|
||||
<small>
|
||||
<code>domain/</code> <code>application/</code> <code>infrastructure/</code>
|
||||
<code>presentation/</code> <code>platform/</code> <code>tests/fakes/</code> —
|
||||
hiện tại <b>chưa tồn tại, 0 file</b>. Mọi task của cả ba người đều ghi vào đây; để ba
|
||||
người tự tạo là đụng nhau ở <code>__init__.py</code> ngay ngày đầu.
|
||||
</small>
|
||||
</li>
|
||||
<li>
|
||||
<span class="est">~45 phút</span>
|
||||
<b>Viết interface + fake cho Config và Secrets</b>
|
||||
<small>
|
||||
<code>SecretStore</code>, <code>ConfigRepository</code>, kèm
|
||||
<code>FakeSecretStore</code> và <code>FakeConfigRepository</code>. Chỉ chữ ký, chưa cần
|
||||
thân hàm. Đây là thứ gỡ chốt cho cả hai người kia — <b>156 lời gọi
|
||||
<code>ctx.config.*</code> trong 29 file</b> đang chờ nó.
|
||||
</small>
|
||||
</li>
|
||||
<li>
|
||||
<span class="est">~20 phút</span>
|
||||
<b>Chốt số phận <code>api_key</code> và báo Team Duy</b>
|
||||
<small>
|
||||
<code>provider_conf()</code> còn trả <code>api_key</code> bên trong, hay tách hẳn sang
|
||||
<code>SecretStore</code>? Có 5 nơi đọc trực tiếp, <b>3 trong số đó nằm trong
|
||||
<code>providers/</code> của Team Duy</b>. Quyết một mình rồi im lặng là làm vỡ code
|
||||
team bạn.
|
||||
</small>
|
||||
</li>
|
||||
<li>
|
||||
<span class="est">~30 phút</span>
|
||||
<b>Viết <code>scripts/audit_security.py</code> (CASAN Check 1)</b>
|
||||
<small>
|
||||
Gamma chủ trì check này ngày 30/08. Viết ngay hôm nay thì lead tự kiểm được trong suốt
|
||||
quá trình chuyển API key, thay vì tới ngày cổng mới chạy lần đầu và phát hiện vấn đề.
|
||||
</small>
|
||||
</li>
|
||||
<li>
|
||||
<span class="est">~20 phút</span>
|
||||
<b>Thêm 3 check CASAN vào CI</b>
|
||||
<small>
|
||||
CI hiện chỉ chạy <code>pytest tests -q</code>. Ba check (secret · ≤400 dòng · import
|
||||
guard) không nằm trong CI, nên tới 30/08 mới biết ai vi phạm. Đưa vào CI thì mỗi PR tự
|
||||
báo.
|
||||
</small>
|
||||
</li>
|
||||
<li>
|
||||
<span class="est">~30 phút</span>
|
||||
<b>Quyết số phận 24 checker UI, rồi thông báo</b>
|
||||
<small>
|
||||
Chúng bám vào <code>cowork_local.config</code> (34 chỗ) và <code>cowork_local.app</code>
|
||||
(16 chỗ) — <b>sẽ chết ngay khi lead đụng <code>config.py</code></b>. Đây là lưới an toàn
|
||||
duy nhất cho phần UI vừa làm xong. Xem mục quy ước bên dưới.
|
||||
</small>
|
||||
</li>
|
||||
</ol>
|
||||
</section>
|
||||
|
||||
<h2>Ba làn</h2>
|
||||
<p class="sub">
|
||||
Ba làn ngang nhau, mỗi làn khoảng 2.700 dòng phải bóc tách, <b>cùng đẩy vào một
|
||||
nhánh</b> <code>gamma/refactor</code>. Nam nhận làn N1 vì đó là làn chạm tới file
|
||||
dùng chung nhiều nhất. Cột “sở hữu” là danh sách file <em>chỉ</em> người đó được
|
||||
sửa — trên nhánh chung, đây là thứ duy nhất giữ cho ba người không giẫm chân.
|
||||
</p>
|
||||
|
||||
<div class="cards">
|
||||
|
||||
<div class="card lead">
|
||||
<div class="tag">Làn N1 · Nam</div>
|
||||
<h3>Cấu hình, Bí mật & Vỏ ứng dụng</h3>
|
||||
<p class="who">Nam giữ — làn chạm nhiều file dùng chung nhất</p>
|
||||
<div class="branch">gamma/refactor</div>
|
||||
|
||||
<h4>Việc</h4>
|
||||
<ul>
|
||||
<li><span class="tid">R02-T01…T06</span> AtomicJsonFile · ConfigRepository · Typed Settings Facade · SecretStore + Keyring · chuyển API key · schema versioning</li>
|
||||
<li><span class="tid">R08-T07</span> tách <code>settings_dialog.py</code> → 4 section widget</li>
|
||||
<li><span class="tid">R08-T10</span> <code>bootstrap.py</code> + tách <code>MainWindow</code> → shell · tray · lifecycle <em>(cuối sprint, lắp factory của hai người kia)</em></li>
|
||||
<li><span class="tid">R09-T01</span> tài liệu Security Policy Model</li>
|
||||
<li>Chủ trì <b>CASAN Check 1</b> · giữ CI · duyệt PR của hai người</li>
|
||||
</ul>
|
||||
|
||||
<h4>Sở hữu độc quyền</h4>
|
||||
<ul class="paths">
|
||||
<li>config.py</li>
|
||||
<li>app.py → presentation/shell/</li>
|
||||
<li>bootstrap.py</li>
|
||||
<li>theme.py · i18n.py</li>
|
||||
<li>infrastructure/config/ · secrets/ · persistence/</li>
|
||||
<li>ui/settings_dialog.py → presentation/settings/</li>
|
||||
<li>scripts/ · .gitea/workflows/</li>
|
||||
</ul>
|
||||
|
||||
<p class="weight"><b>~2.700 dòng</b> · 727 settings + 1.352 app + 616 config<br>+ mục chung ở trên</p>
|
||||
</div>
|
||||
|
||||
<div class="card one">
|
||||
<div class="tag">Làn N2 · Hiệp</div>
|
||||
<h3>Giám sát & Quan trắc</h3>
|
||||
<p class="who">Hiệp — 7 tab, việc lặp cần kỷ luật</p>
|
||||
<div class="branch">gamma/refactor</div>
|
||||
|
||||
<h4>Việc</h4>
|
||||
<ul>
|
||||
<li><span class="tid">R08-T08</span> tách <code>monitoring_tab.py</code> → 7 tab độc lập</li>
|
||||
<li><span class="tid">R09-T04</span> <code>CanonicalAuditLogger</code></li>
|
||||
<li><span class="tid">R09-T05</span> <code>MonitoringQueryService</code> read-only, phân trang</li>
|
||||
<li><span class="tid">R09-T02</span> gỡ vòng lặp <code>model_pricing</code> ↔ <code>usage_tracker</code></li>
|
||||
<li><span class="tid">R09-T03</span> gỡ vòng lặp <code>agent_security</code> ↔ <code>alert</code></li>
|
||||
<li><span class="tid">R09-T06</span> ma trận Sandbox theo hệ điều hành</li>
|
||||
</ul>
|
||||
|
||||
<h4>Sở hữu độc quyền</h4>
|
||||
<ul class="paths">
|
||||
<li>ui/monitoring_tab.py → presentation/monitoring/</li>
|
||||
<li>application/monitoring/</li>
|
||||
<li>infrastructure/telemetry/ · sandbox/</li>
|
||||
<li>core/audit_log.py</li>
|
||||
<li>core/model_pricing.py · usage_tracker.py</li>
|
||||
<li>core/agent_security*.py</li>
|
||||
</ul>
|
||||
|
||||
<p class="weight"><b>~2.650 dòng</b> · 1.545 monitoring + ~1.100 core</p>
|
||||
</div>
|
||||
|
||||
<div class="card two">
|
||||
<div class="tag">Làn N3 · Lâm</div>
|
||||
<h3>Co4E Studio</h3>
|
||||
<p class="who">Lâm — canvas và luồng chạy workflow</p>
|
||||
<div class="branch">gamma/refactor</div>
|
||||
|
||||
<h4>Việc</h4>
|
||||
<ul>
|
||||
<li><span class="tid">R07-T06</span> <code>Co4EWorkflowService</code> thuần Python</li>
|
||||
<li><span class="tid">R08-T09</span> tách <code>co4e_tab.py</code> + <code>co4e_canvas.py</code> → canvas · node property · run control · chat view · agent list</li>
|
||||
<li>Gọi tool qua <code>ToolPolicyGateway</code> của Team Hoa — dùng fake, không chờ</li>
|
||||
</ul>
|
||||
|
||||
<h4>Sở hữu độc quyền</h4>
|
||||
<ul class="paths">
|
||||
<li>ui/co4e_tab.py → presentation/co4e/</li>
|
||||
<li>ui/co4e_canvas.py</li>
|
||||
<li>ui/co4e_config_panel.py</li>
|
||||
<li>application/workflows/</li>
|
||||
<li>domain/workflows/</li>
|
||||
<li>core/co4e_run_manager.py</li>
|
||||
</ul>
|
||||
|
||||
<p class="weight"><b>~2.880 dòng</b> · file to nhất của cả team</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<h2>Tám quy ước</h2>
|
||||
<p class="sub">
|
||||
Tám điều dưới đây là luật của team, Nam chốt. Bốn điều đầu là bắt buộc — trên một
|
||||
nhánh chung, vi phạm không chỉ hại mình mà chặn cả hai người kia.
|
||||
</p>
|
||||
|
||||
<div class="rules">
|
||||
|
||||
<div class="rule hard">
|
||||
<h3>1 · Không chạm file dùng chung</h3>
|
||||
<p>
|
||||
<code>app.py</code>, <code>theme.py</code>, <code>i18n.py</code>, <code>config.py</code>,
|
||||
<code>bootstrap.py</code> thuộc nhánh N1 của Nam. Cần thêm chuỗi hay token màu thì
|
||||
<b>nhắn, đừng sửa</b> — Nam thêm trong ngày. Đây là ba file duy nhất có thể gây conflict
|
||||
thật, và luật này xoá hẳn khả năng đó.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rule hard">
|
||||
<h3>2 · Nộp factory, không tự lắp vào app</h3>
|
||||
<p>
|
||||
Mỗi nhánh expose một hàm dựng widget với chữ ký chốt từ ngày đầu, ví dụ
|
||||
<code>build_monitoring_tab(ctx, query_service) -> QWidget</code>. Nam gọi nó trong <code>bootstrap.py</code> ngày 28/08. Không ai tự sửa chỗ khởi tạo trong
|
||||
<code>app.py</code>.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rule hard">
|
||||
<h3>3 · Bị chặn thì dùng fake, không ngồi đợi</h3>
|
||||
<p>
|
||||
Chưa có <code>ConfigRepository</code> bản thật thì dùng <code>FakeConfigRepository</code>.
|
||||
Chưa có <code>ToolPolicyGateway</code> của Team Hoa thì đã có fake sẵn. <b>Báo ngay trong ngày</b>
|
||||
nếu thiếu fake nào — đó là việc của nhóm trưởng, không phải lý do dừng tay.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="rule hard">
|
||||
<h3>4 · Nhánh chung: kéo trước khi đẩy, đừng để nhánh đỏ</h3>
|
||||
<p>
|
||||
Cả ba đẩy vào <code>gamma/refactor</code>, nên không còn nhánh riêng làm vùng
|
||||
đệm. Ba việc bắt buộc: <code>git pull --rebase</code> trước mỗi lần đẩy;
|
||||
commit nhỏ và đẩy trong ngày, đừng ôm 500 dòng ba hôm; và
|
||||
<b>không bao giờ đẩy thứ làm <code>pytest tests -q</code> đỏ</b> — nhánh hỏng
|
||||
là hai người kia đứng hình. Lỡ đẩy nhầm thì sửa ngay hoặc
|
||||
<code>git revert</code>, đừng để qua đêm.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rule soft">
|
||||
<h3>5 · Commit nhỏ, mỗi ngày một lần</h3>
|
||||
<p>
|
||||
Một PR cho một sub-widget hoặc một service, không dồn 7 tab vào một PR cuối tuần. Nhóm
|
||||
trưởng duyệt trong ngày. PR càng to thì rủi ro càng dồn về ngày 28/08.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rule soft">
|
||||
<h3>6 · Mỗi commit kèm test, và không làm đỏ 90 test cũ</h3>
|
||||
<p>
|
||||
Baseline hiện tại: <b>102 test xanh trong 3,4 giây</b>. Chạy <code>pytest tests -q</code>
|
||||
trước mỗi lần đẩy. Đây là lưới an toàn cho phần logic — giữ nó xanh suốt 10 ngày.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rule soft">
|
||||
<h3>7 · File mới ≤ 400 dòng, không import PySide6 vào lõi</h3>
|
||||
<p>
|
||||
Hai điều kiện của CASAN Check 2 và 3. Tự kiểm trước khi đẩy — CI sẽ báo, nhưng biết
|
||||
sớm thì đỡ phải tách lại lần hai.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="rule soft">
|
||||
<h3>8 · Checker UI thuộc phạm vi ai, người đó cập nhật</h3>
|
||||
<p>
|
||||
24 checker sẽ vỡ khi file bị dời. Ai dời file thì sửa checker tương ứng ngay trong
|
||||
commit đó — tốn thêm khoảng 15% thời gian, đổi lại giữ được lưới an toàn cho phần
|
||||
UI vừa làm xong.
|
||||
<b>Đã chốt 21/08: đường A. Nam chịu trách nhiệm nếu đổi ý.</b>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<h2>Mỗi người nhận gì, giao gì</h2>
|
||||
<p class="sub">
|
||||
Cột trái là thứ phải có trong tay mới làm được, kèm nguồn. Cột phải là thứ bắt
|
||||
buộc giao ra, kèm người nhận. Nhãn <span class="frm done">có rồi</span> nghĩa là
|
||||
mục chung đã làm xong.
|
||||
</p>
|
||||
|
||||
<div class="io">
|
||||
|
||||
<div class="iorow n1">
|
||||
<div class="iohead"><b>N1 · Cấu hình, Bí mật & Vỏ</b><span>Nam · nhóm trưởng</span></div>
|
||||
<div class="iogrid">
|
||||
<div>
|
||||
<p class="iocap">Input — cần có</p>
|
||||
<ul class="iolist">
|
||||
<li><span class="frm">mã cũ</span><code>config.py</code> 616 dòng</li>
|
||||
<li><span class="frm">mã cũ</span><code>ui/settings_dialog.py</code> 727 dòng</li>
|
||||
<li><span class="frm">mã cũ</span><code>app.py</code> 1.352 dòng</li>
|
||||
<li><span class="frm risk">tự chốt</span>Quyết định <code>api_key</code> — trước 26/08</li>
|
||||
<li><span class="frm">từ Hiệp</span>Chữ ký <code>build_monitoring_tab()</code> — trước 28/08</li>
|
||||
<li><span class="frm">từ Lâm</span>Chữ ký <code>build_co4e_tab()</code> — trước 28/08</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p class="iocap">Output — phải giao</p>
|
||||
<ul class="iolist">
|
||||
<li><span class="frm done">có rồi</span><code>SecretStore</code> · <code>ConfigRepository</code> + fake → <b>cho Hiệp và Lâm</b></li>
|
||||
<li><span class="frm done">có rồi</span><code>scripts/audit_security.py</code> → cho CI</li>
|
||||
<li><code>infrastructure/persistence/json/atomic_json_file.py</code></li>
|
||||
<li><code>infrastructure/config/</code> — cài đặt thật + settings facade</li>
|
||||
<li><code>infrastructure/secrets/keyring_adapter.py</code></li>
|
||||
<li><code>presentation/settings/</code> — 4 widget</li>
|
||||
<li><code>bootstrap.py</code> + <code>presentation/shell/</code> — 3 file</li>
|
||||
<li><code>docs/architecture/security-policy.md</code></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="iorow n2">
|
||||
<div class="iohead"><b>N2 · Giám sát</b><span>Hiệp</span></div>
|
||||
<div class="iogrid">
|
||||
<div>
|
||||
<p class="iocap">Input — cần có</p>
|
||||
<ul class="iolist">
|
||||
<li><span class="frm">mã cũ</span><code>ui/monitoring_tab.py</code> 1.545 dòng</li>
|
||||
<li><span class="frm">mã cũ</span><code>core/usage_tracker.py</code> 524 · <code>sandbox_manager.py</code> 335</li>
|
||||
<li><span class="frm">mã cũ</span><code>core/model_pricing.py</code> 284 · <code>agent_security.py</code> 272 · <code>audit_log.py</code> 115</li>
|
||||
<li><span class="frm done">từ Nam</span><code>FakeConfigRepository</code> — dùng được ngay</li>
|
||||
<li><span class="frm risk">tự chốt</span>Giữ nguyên 9 trường log, báo Duy và Hoa</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p class="iocap">Output — phải giao</p>
|
||||
<ul class="iolist">
|
||||
<li><code>build_monitoring_tab()</code> → <b>cho Nam</b>, trước 28/08</li>
|
||||
<li><code>FakeAuditLogger</code> · <code>FakeMonitoringQueryService</code> → <b>cho cả team</b></li>
|
||||
<li><code>presentation/monitoring/</code> — 7 tab + shell</li>
|
||||
<li><code>application/monitoring/monitoring_query_service.py</code></li>
|
||||
<li><code>infrastructure/telemetry/audit_logger.py</code></li>
|
||||
<li><code>infrastructure/sandbox/sandbox_capabilities.py</code></li>
|
||||
<li><b>0 circular import</b> ở pricing ↔ usage và security ↔ alert</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="iorow n3">
|
||||
<div class="iohead"><b>N3 · Co4E Studio</b><span>Lâm</span></div>
|
||||
<div class="iogrid">
|
||||
<div>
|
||||
<p class="iocap">Input — cần có</p>
|
||||
<ul class="iolist">
|
||||
<li><span class="frm">mã cũ</span><code>ui/co4e_tab.py</code> 2.089 dòng</li>
|
||||
<li><span class="frm">mã cũ</span><code>ui/co4e_canvas.py</code> 791 · <code>co4e_config_panel.py</code></li>
|
||||
<li><span class="frm">mã cũ</span><code>core/co4e_run_manager.py</code> 331</li>
|
||||
<li><span class="frm">có sẵn</span><code>core/co4e.py</code> — dataclass Workflow/Node/Edge đã có</li>
|
||||
<li><span class="frm done">từ Nam</span><code>FakeConfigRepository</code></li>
|
||||
<li><span class="frm risk">từ Team Hoa</span>DTO <code>ToolPolicyGateway</code> — <b>rủi ro liên team cao nhất</b>, lấy trong hôm nay</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p class="iocap">Output — phải giao</p>
|
||||
<ul class="iolist">
|
||||
<li><code>build_co4e_tab()</code> → <b>cho Nam</b>, trước 28/08</li>
|
||||
<li><code>FakeCo4EWorkflowService</code> → <b>cho cả team</b></li>
|
||||
<li><code>domain/workflows/</code> — DTO chốt ngày đầu</li>
|
||||
<li><code>application/workflows/co4e_workflow_service.py</code></li>
|
||||
<li><code>presentation/co4e/</code> — 5 phần</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<h3>Output bắt buộc với cả ba, mỗi lần đẩy</h3>
|
||||
<div class="scroll">
|
||||
<table>
|
||||
<thead><tr><th>Điều kiện</th><th>Ngưỡng</th><th>Tự kiểm bằng</th></tr></thead>
|
||||
<tbody>
|
||||
<tr><td>File mới sau khi tách</td><td class="mono">≤ 400 dòng</td><td class="mono">wc -l</td></tr>
|
||||
<tr><td><code>domain/</code> và <code>application/</code> import PySide6</td><td class="mono">0</td><td class="mono">grep -r PySide6</td></tr>
|
||||
<tr><td>Test hiện có</td><td class="mono">102 xanh</td><td class="mono">pytest tests -q</td></tr>
|
||||
<tr><td>Credential lộ</td><td class="mono">0</td><td class="mono">python scripts/audit_security.py</td></tr>
|
||||
<tr><td>Checker UI trong phạm vi mình dời</td><td>đã cập nhật</td><td class="mono">python tools/check_<tên>.py</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2>Lịch từng ngày</h2>
|
||||
<p class="sub">Ba hàng chạy độc lập. Hàng tô nền là lúc cả ba phải gặp nhau.</p>
|
||||
|
||||
<div class="scroll">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="day">Ngày</th>
|
||||
<th>N1 · Nam</th>
|
||||
<th>N2 · Hiệp</th>
|
||||
<th>N3 · Lâm</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="day">21/08<br><small>T6</small></td>
|
||||
<td class="cl"><b>Mục chung</b> · dựng khung · interface + fake · chốt api_key · CASAN script<small>Merge trước khi hai người kia bắt đầu</small></td>
|
||||
<td class="c1">Chốt schema log 9 trường<small>Giữ nguyên định dạng cũ để 24 chỗ gọi không phải sửa</small></td>
|
||||
<td class="c2">Chốt chữ ký <code>Co4EWorkflowService</code><small>Nộp cho lead để lắp bootstrap sau</small></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="day">22–23/08<br><small>T7–CN</small></td>
|
||||
<td class="cl">AtomicJsonFile · ConfigRepository · Typed Settings Facade</td>
|
||||
<td class="c1">CanonicalAuditLogger · gỡ vòng lặp pricing ↔ usage</td>
|
||||
<td class="c2">Co4EWorkflowService — CRUD & validate, test không cần Qt</td>
|
||||
</tr>
|
||||
<tr class="mark">
|
||||
<td class="day">23/08<br><small>17:00</small></td>
|
||||
<td colspan="3"><span class="pill cp">Checkpoint 1</span> 100% DTO và fake xong · <code>pytest</code> xanh · không ai bị chặn</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="day">24/08<br><small>T2</small></td>
|
||||
<td class="cl">Tách settings: provider + connector widget</td>
|
||||
<td class="c1">3 tab đầu: overview · sandbox · security events</td>
|
||||
<td class="c2">node_property_panel · agent_list_panel</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="day">25/08<br><small>T3</small></td>
|
||||
<td class="cl">Tách settings: routing + general widget</td>
|
||||
<td class="c1">4 tab còn lại: MCP · action logs · agent status · security settings</td>
|
||||
<td class="c2">co4e_canvas_widget — thao tác node</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="day">26/08<br><small>T4</small></td>
|
||||
<td class="cl">Chuyển API key sang SecretStore<small>Báo Team Duy trước khi đụng providers/</small></td>
|
||||
<td class="c1">Lắp shell MonitoringTab · query service bản thật</td>
|
||||
<td class="c2">Run control · chat view</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="day">27/08<br><small>T5</small></td>
|
||||
<td class="cl">Schema versioning · recovery policy</td>
|
||||
<td class="c1">Ma trận Sandbox · gỡ vòng lặp agent_security</td>
|
||||
<td class="c2">Lắp container Co4ETab · thay fake bằng service thật</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="day">28/08<br><small>T6</small></td>
|
||||
<td class="cl"><b>bootstrap.py + tách MainWindow</b><small>Nhận factory của Hiệp và Lâm để lắp</small></td>
|
||||
<td class="c1">Nộp factory · dọn file >400 dòng · cập nhật checker</td>
|
||||
<td class="c2">Nộp factory · dọn file >400 dòng · cập nhật checker</td>
|
||||
</tr>
|
||||
<tr class="mark">
|
||||
<td class="day">28/08<br><small>17:00</small></td>
|
||||
<td colspan="3"><span class="pill cp">Checkpoint 2</span> Tách xong 100% god file · 0 circular import</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="day">29/08<br><small>T7</small></td>
|
||||
<td class="cl">Tài liệu Security Policy · integration test Settings</td>
|
||||
<td class="c1">Integration test Monitoring</td>
|
||||
<td class="c2">Integration test luồng Co4E đầu-cuối</td>
|
||||
</tr>
|
||||
<tr class="mark">
|
||||
<td class="day">30/08<br><small>CN 17:00</small></td>
|
||||
<td colspan="3"><span class="pill gate">CASAN Gate</span> <b>Nam chủ trì Check 1</b> — quét toàn bộ config/JSON, phải ra 0 secret plaintext. Hiệp và Lâm sửa ngay phần của mình nếu script bắt được.</td>
|
||||
</tr>
|
||||
<tr class="mark">
|
||||
<td class="day">31/08<br><small>T2 15:00</small></td>
|
||||
<td colspan="3"><span class="pill ship">Bàn giao</span> Fix tồn đọng · cập nhật tài liệu kiến trúc · merge PR cuối · smoke test 5 luồng chính</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<h2>Nghiệm thu: làm sao biết đã thật sự song song</h2>
|
||||
<p class="sub">Không phải “đã họp xong” mà là chạy được. Ba câu hỏi, trả lời bằng lệnh.</p>
|
||||
|
||||
<div class="scroll">
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Câu hỏi</th><th>Cách trả lời</th><th>Khi nào</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>Hiệp có chạy được khi chưa có config bản thật?</td>
|
||||
<td>Dựng một tab Monitoring, chạy test của nó, <b>không import <code>cowork_local.config</code></b> dòng nào — chỉ dùng <code>FakeConfigRepository</code></td>
|
||||
<td class="mono">21/08</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Lâm có chạy được khi Team Hoa chưa xong gateway?</td>
|
||||
<td>Test <code>Co4EWorkflowService</code> xanh với <code>FakeToolPolicyGateway</code></td>
|
||||
<td class="mono">23/08</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Ba người có đụng file nhau không?</td>
|
||||
<td><code>git log --name-only --pretty=%an</code> trên <code>gamma/refactor</code> —
|
||||
không file nào được xuất hiện dưới hai tên khác nhau</td>
|
||||
<td class="mono">mỗi ngày</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<footer>
|
||||
Nguồn: <code>docs/refactor/plan.md</code>, <code>Refactoring_Checklist.md</code>,
|
||||
<code>Feature_Architecture_Proposal.md</code>. Số dòng code, số lời gọi và baseline test đo
|
||||
trực tiếp trên nhánh <code>main</code> ngày 21/08.
|
||||
Mục chung, cách chia ba nhánh, bảy quy ước và mục nghiệm thu là đề xuất — không có trong
|
||||
tài liệu gốc.
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,189 @@
|
||||
# Quyết định của Team Gamma
|
||||
|
||||
Team: **Nam** (nhóm trưởng, nhánh N1) · **Hiệp** (N2) · **Lâm** (N3).
|
||||
|
||||
Ghi ở đây thay vì chôn trong comment, vì cả ba đều ảnh hưởng ra ngoài phạm vi
|
||||
một người.
|
||||
|
||||
| # | Việc | Trạng thái |
|
||||
|---|---|---|
|
||||
| 1 | `provider_conf()` còn trả `api_key` | **Chốt 21/08 — đường A** |
|
||||
| 2 | Số phận 24 checker UI | **Chốt 21/08 — đường A** |
|
||||
| 3 | DTO `ToolPolicyGateway` viết hộ Team Hoa | Đã làm, chờ Hoa xác nhận |
|
||||
|
||||
---
|
||||
|
||||
## Quyết định 1 — `provider_conf()` còn trả `api_key` hay không
|
||||
|
||||
### Vì sao phải quyết trước khi code
|
||||
|
||||
R02-T05 chuyển API key sang Keyring. Câu hỏi là sau khi chuyển, dict do
|
||||
`provider_conf()` trả về **còn chứa `api_key` không**.
|
||||
|
||||
Có 5 nơi đang đọc trực tiếp — đo trên `main` ngày 21/08:
|
||||
|
||||
| Nơi đọc | Thuộc |
|
||||
|---|---|
|
||||
| `providers/anthropic.py:26` | **Team Duy** |
|
||||
| `providers/openai_compat.py:36` | **Team Duy** |
|
||||
| `core/image_gen.py:50` | Team Duy (routing/model) |
|
||||
| `core/ext_connectors.py:98` | Team Hoa |
|
||||
| `ui/ext_connector_dialog.py:87` | Team Gamma |
|
||||
|
||||
Ba trong năm nằm ngoài team. Quyết một mình rồi im lặng là làm vỡ code người khác.
|
||||
|
||||
### Hai đường
|
||||
|
||||
**A. Giữ `api_key` trong dict, `ConfigRepository` tự lấy từ `SecretStore` rồi ghép vào**
|
||||
|
||||
- 5 nơi đọc **không phải sửa dòng nào**
|
||||
- Không cần báo team khác, không cần đồng bộ lịch
|
||||
- Đổi lại: bí mật vẫn đi lang thang trong dict, dễ lọt vào log hoặc màn hình debug
|
||||
- CASAN Check 1 vẫn PASS vì nó quét **file trên đĩa**, không quét bộ nhớ
|
||||
|
||||
**B. Bỏ `api_key` khỏi dict, ai cần thì gọi `secrets.get(provider_key(name))`**
|
||||
|
||||
- Sạch về nguyên tắc: bí mật chỉ xuất hiện đúng chỗ cần
|
||||
- Đổi lại: **5 nơi phải sửa**, 3 trong đó phải chờ team khác xếp lịch
|
||||
- Rủi ro: quên một chỗ thì mất API key lúc chạy thật, mà test có fake nên không bắt được
|
||||
|
||||
### Đề xuất
|
||||
|
||||
**Đường A cho sprint này, đường B ghi vào nợ kỹ thuật.**
|
||||
|
||||
Lý do: mục tiêu của cổng CASAN là *không còn secret nằm trên đĩa*, và đường A
|
||||
đạt được điều đó. Đường B giải quyết thêm chuyện secret trong bộ nhớ — đúng
|
||||
nhưng không phải việc của 10 ngày này, và nó kéo hai team khác vào một thay đổi
|
||||
họ không lên kế hoạch.
|
||||
|
||||
Nếu chọn B thì **phải báo Team Duy và Team Hoa trong hôm nay**, không phải lúc
|
||||
đã sửa xong.
|
||||
|
||||
> **Nam chốt 21/08: đường A.**
|
||||
>
|
||||
> Việc kèm theo: `ConfigRepository` bản thật phải đọc key từ `SecretStore` rồi
|
||||
> ghép vào dict do `provider_conf()` trả về. Năm nơi đọc không đổi một dòng,
|
||||
> nên **không cần báo Duy và Hoa**.
|
||||
>
|
||||
> Nợ kỹ thuật đã ghi: đường B (bỏ `api_key` khỏi dict) để sau sprint này.
|
||||
|
||||
---
|
||||
|
||||
## Quyết định 2 — số phận 24 checker UI
|
||||
|
||||
### Vấn đề
|
||||
|
||||
`tools/check_*.py` là bộ kiểm tra giao diện viết trong 2 tuần vừa rồi, hiện
|
||||
**24 file**. Chúng bám vào đường dẫn cũ:
|
||||
|
||||
| Import | Số chỗ |
|
||||
|---|---|
|
||||
| `cowork_local.config` | 34 |
|
||||
| `cowork_local.app` | 16 |
|
||||
| `cowork_local.state` | 22 |
|
||||
| `cowork_local.ui.*` | ~12 |
|
||||
|
||||
R08 dời hết những module đó sang `presentation/`. Nghĩa là **cả 24 checker chết
|
||||
ngay ngày N1 đụng `config.py`** — và đó là lưới an toàn duy nhất cho phần giao
|
||||
diện, vì `pytest` không kiểm giao diện (90 test hiện tại là logic).
|
||||
|
||||
### Ba đường
|
||||
|
||||
**A. Ai dời file thì cập nhật checker tương ứng, ngay trong PR đó**
|
||||
|
||||
- Giữ được lưới suốt 10 ngày
|
||||
- Tốn thêm ~15% thời gian mỗi PR
|
||||
- Rủi ro: người sửa vội có thể nới lỏng phép kiểm cho nó xanh — đã xảy ra một
|
||||
lần trong quá trình làm UI, khi một checker được sửa thành *không thể đỏ*
|
||||
|
||||
**B. Đóng băng: bỏ khỏi CI, sửa một lượt ngày 31/08**
|
||||
|
||||
- Nhanh nhất trong 10 ngày
|
||||
- Đổi lại: **không có gì canh hồi quy giao diện** suốt cả sprint. Refactor là lúc
|
||||
dễ vỡ giao diện nhất
|
||||
- Rủi ro cuối sprint: sửa 24 file cùng lúc, không ai nhớ cái nào đo gì
|
||||
|
||||
**C. Bỏ hẳn**
|
||||
|
||||
Không khuyến nghị. Vứt đi hai tuần công sức kiểm chứng, và ba tài liệu refactor
|
||||
không có gì thay thế cho phần giao diện.
|
||||
|
||||
### Đề xuất
|
||||
|
||||
**Đường A**, kèm một ràng buộc: PR nào *sửa* checker phải nói rõ trong mô tả
|
||||
**sửa gì và vì sao** — để việc nới lỏng phép kiểm không lọt qua review.
|
||||
|
||||
`tools/check_probes_bite.py` đã có sẵn cơ chế chứng minh checker còn cắn được;
|
||||
chạy nó sau mỗi đợt sửa là bắt được ngay chuyện đó.
|
||||
|
||||
> **Chốt 21/08: đường A** — ai dời file thì cập nhật checker tương ứng ngay
|
||||
> trong PR đó.
|
||||
>
|
||||
> Kèm hai ràng buộc, vì rủi ro của đường A là người sửa vội nới lỏng phép kiểm:
|
||||
>
|
||||
> 1. PR nào *sửa* checker phải nói rõ trong mô tả **sửa gì và vì sao**.
|
||||
> 2. Sửa xong chạy `python tools/check_probes_bite.py` — nó cắm lỗi cố ý vào
|
||||
> code rồi kiểm checker có bắt được không. Chính công cụ này đã từng bắt
|
||||
> được một checker bị sửa thành *không thể đỏ*.
|
||||
>
|
||||
> Không đưa 24 checker vào CI trong sprint này: chúng dựng `MainWindow` thật,
|
||||
> mỗi lần chạy tốn hàng chục giây và thỉnh thoảng sập lúc Qt dọn dẹp. Chạy tay
|
||||
> theo phạm vi mình đụng là đủ.
|
||||
|
||||
---
|
||||
|
||||
## Quyết định 3 — Gamma viết hộ DTO `ToolPolicyGateway` cho Team Hoa
|
||||
|
||||
**Đã làm, chờ Hoa xác nhận.** Ngày: 21/08.
|
||||
|
||||
### Vì sao làm thay
|
||||
|
||||
N3 (Co4E) cần gọi tool nhưng Team Hoa chưa bắt đầu. Ba đường:
|
||||
|
||||
| | Hệ quả |
|
||||
|---|---|
|
||||
| N3 ngồi đợi Hoa | Mất mấy ngày, trái nguyên tắc "không team nào chặn team nào" |
|
||||
| N3 tự phỏng đoán | Phỏng đoán của một người, không ai soi, sửa lại chắc chắn |
|
||||
| **Gamma viết bản đề xuất** | N3 chạy ngay, Hoa có cái cụ thể để duyệt hoặc sửa |
|
||||
|
||||
### Ranh giới không lấn
|
||||
|
||||
Sơ đồ phân hệ trong `plan.md` giao `domain/security/` cho **Team Gamma**, còn
|
||||
`application/conversations/tool_policy_gateway.py` cho **Team Hoa**.
|
||||
|
||||
Nên chia đúng như vậy:
|
||||
|
||||
- **Gamma định nghĩa hình dạng** → `domain/security/tool_policy.py`
|
||||
- **Hoa cài đặt gateway** → `application/conversations/tool_policy_gateway.py`,
|
||||
nối vào `core/mcp_client.py` và tool dựng sẵn
|
||||
|
||||
Không đụng file nào của Hoa.
|
||||
|
||||
### Đã bám vào code đang chạy, không bịa
|
||||
|
||||
| Nguồn | Lấy gì |
|
||||
|---|---|
|
||||
| `core/agent_security.py::SecurityVerdict` | `allowed` · `reason` · `layer` |
|
||||
| `ui/permission_dialog.py` + `chat_panel.py:1312` | trạng thái "hỏi người dùng" |
|
||||
|
||||
Khác biệt duy nhất: gộp thành **một câu trả lời ba trạng thái**
|
||||
(`ALLOW` / `DENY` / `ASK`) thay vì bắt chỗ gọi tự nhớ hỏi hai nơi.
|
||||
|
||||
Hai ràng buộc đưa vào có chủ đích:
|
||||
|
||||
1. `DENY` và `ASK` **bắt buộc có `reason`** — người dùng cần biết vì sao, và
|
||||
`audit_log` cần ghi lại. Thiếu là ném lỗi ngay lúc dựng, không phải lúc chạy.
|
||||
2. `ASK` **không phải** `allowed` — bẫy dễ mắc nhất là coi ASK như ALLOW rồi tool
|
||||
chạy mà chưa ai đồng ý. Có test riêng cho chuyện này.
|
||||
|
||||
### Gửi Hoa cái gì
|
||||
|
||||
> Bên mình viết trước bản đề xuất `ToolPolicyGateway` ở
|
||||
> `domain/security/tool_policy.py` vì N3 cần gọi tool mà bên Hoa chưa bắt đầu —
|
||||
> để N3 khỏi phải tự đoán. Ba kiểu: `ToolCallRequest`, `PolicyDecision`,
|
||||
> `ToolPolicyGateway`. Phần cài đặt vẫn để bên Hoa ở
|
||||
> `application/conversations/tool_policy_gateway.py`, bọn mình không đụng.
|
||||
> Thấy chỗ nào không hợp thì sửa thẳng file đó, đừng tạo kiểu thứ hai. Đổi bây
|
||||
> giờ còn rẻ vì mới mình N3 dùng.
|
||||
|
||||
> Đã gửi Hoa: ☐ — ngày ____ Hoa xác nhận: ☐ đồng ý ☐ có sửa
|
||||
@@ -26,16 +26,16 @@
|
||||
* **Team chịu trách nhiệm**: 🔵 **Team Duy** (Chủ trì ADR & Test Doubles) + Phối hợp cả 3 team
|
||||
* **Mục tiêu**: Khóa DTO, dựng fakes/test doubles chạy offline không phụ thuộc Qt/mạng, thiết lập script chặn vi phạm kiến trúc.
|
||||
|
||||
- [ ] **R01-T01 (Team Duy)**: Viết Architecture ADR định rõ ranh giới các tầng ➔ `docs/architecture/ADR-001-layered-architecture.md`
|
||||
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
|
||||
- [ ] **R01-T02 (Team Duy)**: Xây dựng `FakeProvider` và `FakeToolExecutor` chạy offline từ `providers/base.py` ➔ `tests/fakes/fake_provider.py` & `tests/fakes/fake_tool_executor.py`
|
||||
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
|
||||
- [ ] **R01-T03 (Team Duy)**: Viết script quét tĩnh chặn code mới trong `domain/` và `application/` import `PySide6` ➔ `scripts/check_imports.py`
|
||||
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
|
||||
- [ ] **R01-T04 (Team Duy)**: Viết Characterization Tests cho `core/chat_agent.py::run_cowork` ➔ `tests/characterization/test_run_cowork.py`
|
||||
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
|
||||
- [ ] **R01-T05 (Team Duy)**: Lập danh mục và phân loại mã nguồn dormant/dead code ➔ `docs/architecture/dormant-code.md`
|
||||
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
|
||||
- [x] **R01-T01 (Team Duy)**: Viết Architecture ADR định rõ ranh giới các tầng ➔ `docs/architecture/ADR-001-layered-architecture.md`
|
||||
*Start: `2026-08-21 18:23` | End: `2026-08-21 18:24`*
|
||||
- [x] **R01-T02 (Team Duy)**: Xây dựng `FakeProvider` và `FakeToolExecutor` chạy offline từ `providers/base.py` ➔ `tests/fakes/fake_provider.py` & `tests/fakes/fake_tool_executor.py`
|
||||
*Start: `2026-08-21 18:24` | End: `2026-08-21 18:26`*
|
||||
- [x] **R01-T03 (Team Duy)**: Viết script quét tĩnh chặn code mới trong `domain/` và `application/` import `PySide6` ➔ `scripts/check_imports.py`
|
||||
*Start: `2026-08-21 18:26` | End: `2026-08-21 18:28`*
|
||||
- [x] **R01-T04 (Team Duy)**: Viết Characterization Tests cho `core/chat_agent.py::run_cowork` ➔ `tests/characterization/test_run_cowork.py`
|
||||
*Start: `2026-08-21 18:28` | End: `2026-08-21 18:32`*
|
||||
- [x] **R01-T05 (Team Duy)**: Lập danh mục và phân loại mã nguồn dormant/dead code ➔ `docs/architecture/dormant-code.md`
|
||||
*Start: `2026-08-21 18:32` | End: `2026-08-21 18:35`*
|
||||
|
||||
---
|
||||
|
||||
@@ -62,18 +62,72 @@
|
||||
* **Team chịu trách nhiệm**: 🔵 **Team Duy** (Chủ trì)
|
||||
* **Mục tiêu**: Hợp nhất logic routing bị phân tán thành `RoutingApplicationService` độc lập Qt; chuẩn hóa catalog nhà cung cấp.
|
||||
|
||||
- [ ] **R03-T01 (Team Duy)**: Xây dựng bộ Contract Tests chuẩn hóa cho các Provider từ `providers/base.py` ➔ `tests/contracts/test_providers.py`
|
||||
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
|
||||
- [ ] **R03-T02 (Team Duy)**: Xây dựng `ProviderDescriptor` và `ProviderRegistry` tập trung từ `providers/factory.py` ➔ `domain/models/provider_descriptor.py` & `infrastructure/providers/provider_registry.py`
|
||||
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
|
||||
- [ ] **R03-T03 (Team Duy)**: Xây dựng `RoutingApplicationService` độc lập với Qt từ `core/routing/` ➔ `application/model_routing/routing_application_service.py`
|
||||
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
|
||||
- [ ] **R03-T04 (Team Duy)**: Di chuyển luồng gọi routing từ `ui/chat_panel.py#L638` sang `RoutingApplicationService`
|
||||
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
|
||||
- [ ] **R03-T05 (Team Duy)**: Di chuyển luồng gọi routing từ `ui/co4e_tab.py` và `ui/folder_tab.py` sang `RoutingApplicationService`
|
||||
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
|
||||
- [ ] **R03-T06 (Team Duy)**: Tách logic ghi nhận token usage ra khỏi Provider, chuyển thành `UsageEventSink` ➔ `infrastructure/telemetry/usage_sink.py`
|
||||
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
|
||||
- [x] **R03-T01 (Team Duy)**: Xây dựng bộ Contract Tests chuẩn hóa cho các Provider từ `providers/base.py` ➔ `tests/contracts/test_providers.py`
|
||||
*Start: `2026-08-22 18:59` | End: `2026-08-22 19:01`*
|
||||
- [x] **R03-T02 (Team Duy)**: Xây dựng `ProviderDescriptor` và `ProviderRegistry` tập trung từ `providers/factory.py` ➔ `domain/models/provider_descriptor.py` & `infrastructure/providers/provider_registry.py`
|
||||
*Start: `2026-08-22 18:45` | End: `2026-08-22 18:50`*
|
||||
- [x] **R03-T03 (Team Duy)**: Xây dựng `RoutingApplicationService` độc lập với Qt từ `core/routing/` ➔ `application/model_routing/routing_application_service.py`
|
||||
*Start: `2026-08-22 18:53` | End: `2026-08-22 18:57`*
|
||||
- [x] **R03-T04 (Team Duy)**: Di chuyển luồng gọi routing từ `ui/chat_panel.py#L638` sang `RoutingApplicationService`
|
||||
*Start: `2026-08-22 18:57` | End: `2026-08-22 18:58`*
|
||||
- [x] **R03-T05 (Team Duy)**: Di chuyển luồng gọi routing từ `ui/co4e_tab.py` và `ui/folder_tab.py` sang `RoutingApplicationService`
|
||||
*Start: `2026-08-22 18:58` | End: `2026-08-22 18:59`*
|
||||
- [x] **R03-T06 (Team Duy)**: Tách logic ghi nhận token usage ra khỏi Provider, chuyển thành `UsageEventSink` ➔ `infrastructure/telemetry/usage_sink.py`
|
||||
*Start: `2026-08-22 18:50` | End: `2026-08-22 18:53`*
|
||||
|
||||
#### 📦 KẾT QUẢ THỰC HIỆN EPIC R03 (Hoàn tất 2026-08-22 19:01 — nhánh `feature/delta-team/epic-R03`)
|
||||
|
||||
**File sản phẩm mới (tất cả < 400 dòng, 100% comment tiếng Anh):**
|
||||
|
||||
| Task | File | LOC | Nội dung chính |
|
||||
| :--- | :--- | :---: | :--- |
|
||||
| T02 | `domain/models/provider_descriptor.py` | 196 | `ProviderDescriptor` (frozen dataclass), `WireProtocol`, `AuthKind`; giá/context để `None` khi chưa biết thay vì đoán bừa |
|
||||
| T02 | `infrastructure/providers/provider_registry.py` | 287 | `ProviderRegistry` thread-safe: tra cứu theo id/alias, **tra cứu động theo model ID** (`find_by_model`), dựng adapter theo wire protocol; `BUILTIN_DESCRIPTORS` cho 5 provider |
|
||||
| T03 | `application/model_routing/routing_models.py` | 158 | DTO thuần Python: `RoutingMode` (Off/Auto/Manual/**Fallback**), `RoutingRequest` (immutable snapshot), `RouteEvaluation`, `RoutingOutcome` |
|
||||
| T03 | `application/model_routing/routing_application_service.py` | 236 | `RoutingApplicationService` — 1 nơi duy nhất quyết định routing; 2 port hẹp (`RoutingDecisionPort`, `ModeResolver`) + callback confirm ⇒ 0 phụ thuộc Qt |
|
||||
| T03 | `application/model_routing/core_routing_adapter.py` | 169 | `CoreRoutingEngine` (cầu nối sang `core/routing`), `AppContextModeResolver`, `build_routing_application_service(ctx)` (cache 1 instance/ctx) |
|
||||
| T06 | `infrastructure/telemetry/usage_sink.py` | 288 | `UsageEvent` + `UsageEventSink` (Protocol) + `UsageTrackerSink` / `InMemoryUsageSink` / `CompositeUsageSink`; publish không bao giờ raise |
|
||||
|
||||
**File hiện hữu được sửa (đều có comment tiếng Anh tại mọi khối thay đổi):**
|
||||
|
||||
| File | Thay đổi |
|
||||
| :--- | :--- |
|
||||
| `providers/factory.py` | Bỏ bảng `_REGISTRY` nội bộ, ủy quyền cho `ProviderRegistry`; vẫn raise `ProviderError` để không vỡ call site cũ |
|
||||
| `providers/openai_compat.py`, `providers/anthropic.py` | Không còn gọi thẳng `core/usage_tracker`; chỉ **publish** `UsageEvent` qua sink (T06) |
|
||||
| `ui/chat_panel.py` (#L638), `ui/co4e_tab.py`, `ui/folder_tab.py` | Xóa 3 bản sao logic routing (~35 dòng/file) ➔ gọi chung `RoutingApplicationService` (T04, T05); widget chỉ còn dựng `RoutingRequest`, host modal confirm và render kết quả |
|
||||
| `config.py`, `state.py`, `ui/routing_toggle.py`, `i18n.py` | Mở đường cho chế độ thứ 4 **Fallback**: hằng `AppConfig.ROUTING_MODES`, validate per-workspace, thêm mục trong combo + chuỗi EN/JA/VI |
|
||||
| `core/usage_tracker.py` | Thêm `current_context()` để sink mượn/trả lại context của thread thay vì gán đè vĩnh viễn |
|
||||
| `tests/conftest.py`, `tests/routing/conftest.py` | **Sửa lỗi hạ tầng test nghiêm trọng** (xem "Ghi chú" bên dưới) |
|
||||
|
||||
**Bộ test bổ sung (tất cả offline, không cần network/Qt):**
|
||||
|
||||
| File | Số test | Phạm vi |
|
||||
| :--- | :---: | :--- |
|
||||
| `tests/contracts/test_providers.py` (+ `provider_stubs.py`) | 50 | Contract chạy parametrize trên **mọi** provider trong registry: signature `chat()`, canonical assistant message, tool call chuẩn hóa, đóng response, dịch tool schema, `ProviderError`, `list_models`/`test_connection`, đúng 1 `UsageEvent`/turn |
|
||||
| `tests/unit/test_routing_application_service.py` | 28 | Đủ 4 chế độ + mọi nhánh degrade (engine lỗi, resolver lỗi, dialog lỗi, thiếu callback) |
|
||||
| `tests/unit/test_provider_registry.py` | 17 | Descriptor + registry + đối chiếu catalogue với `DEFAULT_CONFIG["providers"]` |
|
||||
| `tests/unit/test_core_routing_adapter.py` | 12 | Dịch `RouteResult` ⇄ DTO, task type sai định dạng, thiếu ranking, cache service |
|
||||
| `tests/unit/test_usage_sink.py` | 13 | Fan-out, subscriber lỗi, khôi phục thread context, publish không raise |
|
||||
| `tests/integration/test_routing_unification.py` | 14 | Chạy `RoutingApplicationService` trên **engine `core/routing` thật**; 3 surface (cowork/co4e/ai_edit) cho ra cùng 1 quyết định |
|
||||
|
||||
**Kết quả cổng kiểm duyệt (DoD 7 tiêu chí):**
|
||||
|
||||
| # | Tiêu chí | Lệnh | Kết quả |
|
||||
| :---: | :--- | :--- | :--- |
|
||||
| 1 | LOC < 400 | `wc -l` các file mới | ✅ Lớn nhất 288 dòng (`usage_sink.py`); `openai_compat.py` 374, `anthropic.py` 332 |
|
||||
| 2 | Clean Architecture | `python scripts/check_imports.py` | ✅ `[PASS] 0 forbidden imports detected` |
|
||||
| 3 | Comment tiếng Anh | Review thủ công | ✅ 100% khối code mới/sửa có comment giải thích logic + lý do kiến trúc |
|
||||
| 4 | Có test tự động | `pytest tests/unit tests/contracts tests/integration` | ✅ 134 test mới, pass 100% |
|
||||
| 5 | No Regression | `pytest tests/` | ✅ **236 passed in ~2.0s** (nền trước R03: 102 passed) |
|
||||
| 6 | Timestamps | Bảng trên | ✅ Đã ghi Start/End cho T01–T06 |
|
||||
| 7 | CASAN Gate | `scripts/run_quality_gate.py` | ⚠️ Script **chưa tồn tại** — thuộc R10-T02 (chưa làm). Đã chạy thay bằng `check_imports.py` + `pytest tests/` |
|
||||
|
||||
**Ghi chú kỹ thuật cần biết khi review:**
|
||||
|
||||
1. **Đã sửa 1 lỗi hạ tầng test có thể gây kết quả sai lệch**: `tests/conftest.py` cũ đẩy thư mục **cha** của repo vào `sys.path`, nên `import cowork_local.*` (dùng bởi `tests/routing/*` và `tests/characterization/*`) trỏ sang **một checkout `cowork_local` khác** nằm cạnh thư mục làm việc — test vẫn báo xanh nhưng chạy trên mã nguồn khác. Nay conftest bind thẳng checkout hiện tại vào `sys.modules["cowork_local"]`.
|
||||
2. **Chế độ Fallback** là chế độ *chống gãy*, không phải chế độ tối ưu: giữ nguyên model người dùng chọn kể cả khi có model điểm cao hơn, chỉ chuyển khi model đó **không phục vụ được** turn (không có trong ranking / unavailable / probe fail). Engine `core/routing` không cần biết chế độ này — service map Fallback ➔ Auto khi hỏi ranking rồi tự áp luật chấp nhận riêng.
|
||||
3. **T06 hiện tại**: provider publish `UsageEvent`; khi R04 dựng xong `AgentEvent` bus thì `ConversationApplicationService` sẽ là nơi phát sự kiện, sink giữ nguyên không phải sửa.
|
||||
4. **Cần cài `mcp>=1.0.0`** (đã có trong `requirements.txt`) để `tests/test_project_context_mcp_template.py` collect được — thiếu gói này toàn bộ suite bị interrupt.
|
||||
|
||||
---
|
||||
|
||||
@@ -81,16 +135,22 @@
|
||||
* **Team chịu trách nhiệm**: 🔵 **Team Duy** (Chủ trì)
|
||||
* **Mục tiêu**: Đóng gói input turn chat thành `ConversationExecutionRequest` bất biến, điều phối vòng đời qua `ConversationApplicationService` và phát sinh sự kiện `AgentEvent` có định kiểu.
|
||||
|
||||
- [ ] **R04-T01 (Team Duy)**: Định nghĩa immutable dataclass `ConversationExecutionRequest` ➔ `domain/agents/conversation_execution_request.py`
|
||||
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
|
||||
- [ ] **R04-T02 (Team Duy)**: Chuẩn hóa các sự kiện `AgentEvent` (TextChunk, ToolCallStarted, ToolCallResult, Error) ➔ `domain/agents/agent_event.py`
|
||||
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
|
||||
- [ ] **R04-T03 (Team Duy)**: Xây dựng `ConversationApplicationService` điều phối thực thi từ `core/chat_agent.py` ➔ `application/conversations/conversation_application_service.py`
|
||||
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
|
||||
- [ ] **R04-T04 (Team Duy)**: Di chuyển `ui/cowork_tab.py::build_job` sang sử dụng `ConversationExecutionRequest`
|
||||
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
|
||||
- [ ] **R04-T05 (Team Duy)**: Di chuyển `core/task_executors.py` sang dùng chung `ConversationApplicationService`
|
||||
*Start: `____-__-__ __:__` | End: `____-__-__ __:__`*
|
||||
- [x] **R04-T01 (Team Duy)**: Định nghĩa immutable dataclass `ConversationExecutionRequest` ➔ `domain/agents/conversation_execution_request.py`
|
||||
*Start: `2026-08-23 00:56` | End: `2026-08-23 01:00`*
|
||||
- [x] **R04-T02 (Team Duy)**: Chuẩn hóa các sự kiện `AgentEvent` (TextChunk, ToolCallStarted, ToolCallResult, Error) ➔ `domain/agents/agent_event.py` (+ `domain/agents/agent_event_codec.py` — shim dịch legacy dict, tách riêng để giữ LOC < 400 và để xoá gọn sau R08)
|
||||
*Start: `2026-08-23 01:00` | End: `2026-08-23 01:08`*
|
||||
- [x] **R04-T03 (Team Duy)**: Xây dựng `ConversationApplicationService` điều phối thực thi từ `core/chat_agent.py` ➔ `application/conversations/conversation_application_service.py` (+ `turn_runtime.py` định nghĩa 2 port/6 callable, `core_runtime_adapter.py` cầu nối sang `core/*`, `domain/agents/agent_result.py`)
|
||||
*Start: `2026-08-23 01:08` | End: `2026-08-23 07:10`*
|
||||
Chưa đổi call site nào — `run_cowork` giữ nguyên (Co4E vẫn dùng); việc chuyển call site là T04/T05. Bằng chứng tương đương: `tests/integration/test_conversation_service_parity.py` chạy cùng 1 script provider qua 2 đường và so khớp từng event/message/tool list trên 7 kịch bản.
|
||||
- [x] **R04-T04 (Team Duy)**: Di chuyển `ui/cowork_tab.py::build_job` sang sử dụng `ConversationExecutionRequest`
|
||||
*Start: `2026-08-23 07:10` | End: `2026-08-23 07:23`*
|
||||
`build_job` không còn gọi `run_cowork`: nó chụp state widget tại submit time ➔ `build_cowork_turn_request()` (mới, `application/conversations/cowork_turn_request.py`) ➔ `ConversationApplicationService`. Thêm `combine_instructions()` vào `turn_runtime.py` (project context + admin agent, T05 dùng lại) và tham số `messages=` cho `execute()` để service append vào **đúng list của widget** — `_reattach_running_turn` đọc list đó trong lúc turn đang chạy và `_finalize_turn` slice nó sau đó. Kiểm chứng: `tests/integration/test_cowork_tab_turn.py` gọi thẳng `CoworkTab.build_job` (widget stub, không cần Qt) và chạy turn thật với `FakeProvider`.
|
||||
⚠️ `ui/cowork_tab.py` 416 ➔ 455 LOC — file này **vốn đã vượt 400 trước khi sửa**; phân rã thuộc EPIC R08.
|
||||
- [x] **R04-T05 (Team Duy)**: Di chuyển `core/task_executors.py` sang dùng chung `ConversationApplicationService`
|
||||
*Start: `2026-08-23 07:23` | End: `2026-08-23 07:31`*
|
||||
Nhánh `task_type == "cowork"` của `_run_agent` gọi service thay vì `run_cowork`; 5 hành vi riêng của unattended run giữ nguyên (plan reminder, `history_ready`, autosave History mỗi `assistant_done`, timeout notice, `plan_incomplete_reason`). Tách `_unattended_prompt()` dùng `combine_instructions` để thứ tự reminder → skill → persona → prompt nằm ở 1 chỗ đọc được. Lưới an toàn: `tests/integration/test_task_executor_turn.py` viết **trước** khi migrate và pass 8/8 trên code cũ, vẫn pass sau khi migrate.
|
||||
⚠️ `core/task_executors.py` 476 ➔ 524 LOC — file này **vốn đã vượt 400 trước khi sửa**; phân rã thuộc EPIC R07 (`application/scheduling/`).
|
||||
Còn lại gọi `run_cowork`: `core/co4e_runner.py` (×2) và `ui/co4e_tab.py` — phân hệ Co4E của 🟣 Team Nam, R04 không chạm theo luật 1 file 1 team.
|
||||
|
||||
---
|
||||
|
||||
@@ -229,17 +289,21 @@
|
||||
|
||||
| Ngày | Task Cần Hoàn Thành | Start Time | End Time | Trạng Thái |
|
||||
| :--- | :--- | :---: | :---: | :---: |
|
||||
| **21/08 (T6)** | Khóa DTO `ConversationExecutionRequest`, `AgentEvent`; Xây dựng `FakeProvider`, `FakeToolExecutor` | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
|
||||
| **22-23/08 (T7-CN)** | Chuẩn hóa `ProviderDescriptor`, `ProviderRegistry`; Wrap OpenAI, Anthropic, Ollama, FPT Gateway; Viết Contract Tests | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
|
||||
| **24/08 (T2)** | Xây dựng `RoutingApplicationService` độc lập Qt; Tách `ComposerWidget` & `AttachmentPicker` | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
|
||||
| **21/08 (T6)** | Khóa DTO `ConversationExecutionRequest`, `AgentEvent`; Xây dựng `FakeProvider`, `FakeToolExecutor` | `2026-08-21 18:23` | `2026-08-21 18:35` | [x] |
|
||||
| **22-23/08 (T7-CN)** | Chuẩn hóa `ProviderDescriptor`, `ProviderRegistry`; Wrap OpenAI, Anthropic, Ollama, FPT Gateway; Viết Contract Tests | `2026-08-22 18:45` | `2026-08-22 19:01` | [x] |
|
||||
| **24/08 (T2)** | Xây dựng `RoutingApplicationService` độc lập Qt; Tách `ComposerWidget` & `AttachmentPicker` | `2026-08-22 18:53` | `2026-08-22 18:57` | [~] |
|
||||
| **25/08 (T3)** | Xây dựng `ConversationApplicationService`; Tách `ChatHistoryWidget` và bubble renderer | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
|
||||
| **26/08 (T4)** | Nối stream `AgentEvent` sang Chat History; Tách `AudioRecorderWidget` | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
|
||||
| **27/08 (T5)** | Tách `ChatOutputPanel` & File Watcher; Lắp ráp container `ChatPanel` và `Floating HelpAgent` | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
|
||||
| **28/08 (T6)** | Xóa copy routing cũ trong `ui/chat_panel.py`; Fix circular import `model_pricing` ↔ `usage_tracker` | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
|
||||
| **28/08 (T6)** | Xóa copy routing cũ trong `ui/chat_panel.py`; Fix circular import `model_pricing` ↔ `usage_tracker` | `2026-08-22 18:57` | `2026-08-22 18:59` | [~] |
|
||||
| **29/08 (T7)** | Viết suite integration test cho toàn bộ luồng Chat (`tests/integration/test_chat_flow.py`) | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
|
||||
| **30/08 (CN)** | 🔍 **Chủ trì CASAN Check 3**: Chạy `python scripts/check_imports.py` đảm bảo 0 import `PySide6` trong domain & application | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
|
||||
| **31/08 (T2)** | **Chủ trì EPIC R10**: Viết Contributor Recipes, chạy E2E Smoke Test (`tests/e2e/test_smoke.py`) và merge PR cuối cùng | `____-__-__ __:__` | `____-__-__ __:__` | [ ] |
|
||||
|
||||
> **Chú thích trạng thái**: `[~]` = hoàn tất **phần thuộc EPIC R03**, phần còn lại của dòng đó thuộc EPIC khác nên chưa đóng.
|
||||
> - Dòng **24/08**: đã xong `RoutingApplicationService` (R03-T03); phần `ComposerWidget`/`AttachmentPicker` thuộc R08-T01/T02 — chưa làm.
|
||||
> - Dòng **28/08**: đã xóa copy routing trong `ui/chat_panel.py` (R03-T04) **và** cả `ui/co4e_tab.py`, `ui/folder_tab.py` (R03-T05); phần circular import `model_pricing` ↔ `usage_tracker` thuộc R09-T02 — chưa làm.
|
||||
|
||||
---
|
||||
|
||||
### 🟣 TEAM NAM (Automation Workflows, Co4E, Monitoring & Governance)
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
# NHẬT KÝ THEO DÕI VÀ PHÒNG NGỪA LỖI TÁI CẤU TRÚC (BUG & LESSONS LEARNED LOG)
|
||||
## DỰ ÁN: COWORK LOCAL (COWORK-LOCAL BAMBOO)
|
||||
|
||||
Tài liệu này dùng để ghi nhận **toàn bộ các lỗi, xung đột kiến trúc và sự cố phát sinh** trong suốt quá trình refactoring của cả 3 team (Team Duy, Team Nam, Team Hoa).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> ### 🛡️ NGUYÊN TẮC VÀNG VỀ QUẢN TRỊ CHẤT LƯỢNG (ZERO RECURRENCE):
|
||||
> 1. **Ghi nhận ngay lập tức**: Khi gặp bất kỳ lỗi nào (Syntax, Circular Import, Type Error, Test Failure, Thread Freeze, Data Corruption), kỹ sư/AI phải ghi ngay vào tài liệu này trước khi tiếp tục task.
|
||||
> 2. **Phân tích nguyên nhân gốc rễ (Root Cause)**: Không chỉ sửa phần ngọn mà phải giải thích rõ bản chất vì sao lỗi xảy ra.
|
||||
> 3. **Rút ra quy tắc phòng ngừa (Prevention Rule)**: Đặt ra nguyên tắc kỹ thuật để **TUYỆT ĐỐI KHÔNG TÁI PHẠM** ở các task tiếp theo.
|
||||
> 4. **Checklist đầu vào**: Trước khi bắt đầu bất kỳ task mới nào, kỹ sư/AI **bắt buộc phải đọc lại toàn bộ file này**.
|
||||
|
||||
---
|
||||
|
||||
## 📌 BẢNG TỔNG HỢP CÁC LỖI ĐÃ PHÁT HIỆN & KHẮC PHỤC
|
||||
|
||||
| Bug ID | Ngày Phát Hiện | Phân Hệ / File Bị Ảnh Hưởng | Loại Lỗi | Trạng Thái | Team Phụ Trách |
|
||||
| :--- | :---: | :--- | :--- | :---: | :---: |
|
||||
| `BUG-001` | 2026-08-20 | `core/model_pricing.py` ↔ `core/usage_tracker.py` | Circular Dependency | 🟡 Đã có giải pháp (R09) | Team Duy & Team Nam |
|
||||
| `BUG-002` | 2026-08-20 | `core/agent_security.py` ↔ `core/agent_security_alert.py` | Circular Dependency | 🟡 Đã có giải pháp (R09) | Team Nam |
|
||||
| `BUG-003` | 2026-08-20 | `state.py::active_project_id` & `ui/workspace_tab.py` | Race Condition / Global State Leak | 🟡 Đã có giải pháp (R06) | Team Hoa |
|
||||
| `BUG-004` | 2026-08-20 | `core/task_scheduler.py` ↔ `PySide6.QtCore.QTimer` | Architecture Violation (Qt in Domain/App) | 🟡 Đã có giải pháp (R07) | Team Hoa |
|
||||
| `BUG-005` | 2026-08-20 | `ui/chat_panel.py#L638`, `ui/co4e_tab.py`, `ui/folder_tab.py` | Code Duplication (Copy Routing Logic) | 🟡 Đã có giải pháp (R03) | Team Duy |
|
||||
| `BUG-006` | 2026-08-21 | `scripts/check_imports.py` | UnicodeEncodeError (Windows CP932 console emoji) | 🟢 Đã khắc phục (R01) | Team Duy |
|
||||
| `BUG-007` | 2026-08-21 | `platform/` ➔ `infrastructure/platform/` | Standard Library Shadowing (`import platform`) | 🟢 Đã khắc phục (R01) | Team Duy |
|
||||
|
||||
---
|
||||
|
||||
## 🔍 CHI TIẾT TỪNG LỖI & QUY TẮC PHÒNG NGỪA
|
||||
|
||||
---
|
||||
|
||||
### 🔴 `BUG-001`: Circular Import giữa Module Định Giá (`model_pricing.py`) và Theo Dõi Token (`usage_tracker.py`)
|
||||
|
||||
* **Phân hệ**: `core/model_pricing.py` & `core/usage_tracker.py`
|
||||
* **Triệu chứng (Symptom)**: Lỗi `ImportError: cannot import name 'ModelPricing' from partially initialized module` khi khởi động ứng dụng hoặc chạy test độc lập.
|
||||
* **Nguyên nhân gốc rễ (Root Cause)**:
|
||||
- `model_pricing.py` import `UsageTracker` để cập nhật dữ liệu tiêu thụ.
|
||||
- Ngược lại, `usage_tracker.py` import `ModelPricing` để tính toán chi phí theo từng model ID.
|
||||
* **Giải pháp khắc phục (Resolution)**:
|
||||
- Tách Data Transfer Object (DTO) `ModelPricing` sang tầng Domain thuần túy `domain/models/model_pricing.py`.
|
||||
- Cả `model_pricing.py` và `usage_tracker.py` đều import DTO từ `domain/models/`, chuyển quan hệ thành 1 chiều (Dependency Inversion).
|
||||
* **Quy tắc phòng ngừa (Prevention Rule - TUYỆT ĐỐI KHÔNG TÁI PHẠM)**:
|
||||
> **Quy tắc**: Không bao giờ để 2 service hoặc 2 module nghiệp vụ import lẫn nhau. Mọi cấu trúc dữ liệu dùng chung (DTO/Value Object/Event) **phải được đặt tại tầng `domain/`**.
|
||||
|
||||
---
|
||||
|
||||
### 🔴 `BUG-002`: Circular Import giữa An Ninh Agent (`agent_security.py`) và Cảnh Báo (`agent_security_alert.py`)
|
||||
|
||||
* **Phân hệ**: `core/agent_security.py` & `core/agent_security_alert.py`
|
||||
* **Triệu chứng (Symptom)**: Lỗi khởi tạo vòng tròn khi runtime bắn ra alert sự kiện bảo mật.
|
||||
* **Nguyên nhân gốc rễ (Root Cause)**:
|
||||
- Module security vừa kiểm tra policy vừa khởi tạo trực tiếp instance alert dialog, trong khi alert dialog lại import ngược lại rule security để hiển thị chi tiết mã lỗi.
|
||||
* **Giải pháp khắc phục (Resolution)**:
|
||||
- Tách sự kiện cảnh báo thành Event DTO `SecurityAlertEvent` tại `domain/security/security_event.py`.
|
||||
- Tầng Security chỉ phát ra Event (`emit_event`), tầng Presentation/UI tự lắng nghe Event để render Dialog.
|
||||
* **Quy tắc phòng ngừa (Prevention Rule - TUYỆT ĐỐI KHÔNG TÁI PHẠM)**:
|
||||
> **Quy tắc**: Logic an ninh và xử lý nghiệp vụ không bao giờ được gọi trực tiếp UI Dialog. Luôn giao tiếp thông qua cơ chế Event-Driven (`AgentEvent`, `SecurityEvent`).
|
||||
|
||||
---
|
||||
|
||||
### 🔴 `BUG-003`: Xung Đột Race Condition do Sử Dụng Biến Toàn Cục `active_project_id` trong `state.py`
|
||||
|
||||
* **Phân hệ**: `state.py`, `ui/workspace_tab.py`, Scheduled Task Runners
|
||||
* **Triệu chứng (Symptom)**: Khi task scheduler chạy ngầm hoặc người dùng chuyển tab nhanh, file bị ghi nhầm vào thư mục dự án khác với dự án đang hiển thị trên màn hình.
|
||||
* **Nguyên nhân gốc rễ (Root Cause)**:
|
||||
- Ứng dụng đọc và ghi trực tiếp vào biến toàn cục `AppContext.active_project_id` từ nhiều luồng khác nhau mà không có cơ chế snapshot ngữ cảnh.
|
||||
* **Giải pháp khắc phục (Resolution)**:
|
||||
- Xóa bỏ việc đọc biến toàn cục. Mỗi lần khởi chạy turn hoặc task, tạo một snapshot bất biến `WorkspaceSession(project_id, root_path, allowed_paths)`.
|
||||
- Luồng ngầm chỉ thao tác trên `WorkspaceSession` được truyền vào từ lúc khởi tạo.
|
||||
* **Quy tắc phòng ngừa (Prevention Rule - TUYỆT ĐỐI KHÔNG TÁI PHẠM)**:
|
||||
> **Quy tắc**: Tuyệt đối không dùng biến toàn cục (Global State / Singletons có trạng thái thay đổi) để điều khiển luồng thực thi nền. Mọi ngữ cảnh phải được truyền tường minh qua DTO snapshot.
|
||||
|
||||
---
|
||||
|
||||
### 🔴 `BUG-004`: Vi Phạm Ranh Giới Kiến Trúc Khi Import `PySide6.QtCore.QTimer` trong Domain / Scheduling Engine
|
||||
|
||||
* **Phân hệ**: `core/task_scheduler.py#L20`
|
||||
* **Triệu chứng (Symptom)**: Không thể viết Unit Test cho thuật toán tính toán lịch chạy (cron/interval) trên môi trường CI/CD (GitHub Actions / Linux Server headless) nếu thiếu driver màn hình X11/Wayland hoặc chưa cài `PySide6`.
|
||||
* **Nguyên nhân gốc rễ (Root Cause)**:
|
||||
- Động cơ lập lịch bị gắn chặt cứng với `QTimer` của framework Qt thay vì tách riêng logic tính toán thời gian.
|
||||
* **Giải pháp khắc phục (Resolution)**:
|
||||
- Tách thuật toán tính lịch sang `domain/tasks/schedule_calculator.py` (Pure Python 100%).
|
||||
- Tạo `platform/qt/qt_scheduler_clock.py` làm adapter bọc `QTimer` cho app chạy thật, và `tests/fakes/fake_clock.py` cho unit test.
|
||||
* **Quy tắc phòng ngừa (Prevention Rule - TUYỆT ĐỐI KHÔNG TÁI PHẠM)**:
|
||||
> **Quy tắc**: Tầng Domain và Application tuyệt đối không import thư viện GUI (`PySide6`, `PyQt`). Luôn bọc các thành phần phụ thuộc framework bên ngoài qua Adapter Interface.
|
||||
|
||||
---
|
||||
|
||||
### 🔴 `BUG-005`: Nhân Bản Mã Nguồn (Code Duplication) Logic Routing Mô Hình AI tại Nhiều Màn Hình
|
||||
|
||||
* **Phân hệ**: `ui/chat_panel.py#L638`, `ui/co4e_tab.py`, `ui/folder_tab.py`
|
||||
* **Triệu chứng (Symptom)**: Khi cập nhật thêm model provider mới (như FPT Gateway hay Claude 3.7), phải sửa code thủ công ở 3 file UI khác nhau; phát sinh sai lệch quy tắc fallback giữa các màn hình.
|
||||
* **Nguyên nhân gốc rễ (Root Cause)**:
|
||||
- Thiếu một tầng Application Service tập trung, dẫn đến việc lập trình viên copy-paste hàm chọn model từ `ChatPanel` sang các tab khác.
|
||||
* **Giải pháp khắc phục (Resolution)**:
|
||||
- Xây dựng `application/model_routing/routing_application_service.py` duy nhất, cung cấp API `route_request(request) -> ModelRouteDecision`.
|
||||
- Mọi màn hình UI chỉ gọi service này, không tự viết lại logic kiểm tra key hay fallback.
|
||||
* **Quy tắc phòng ngừa (Prevention Rule - TUYỆT ĐỐI KHÔNG TÁI PHẠM)**:
|
||||
> **Quy tắc**: Nghiệp vụ dùng chung giữa các màn hình phải được đưa vào `application/` services. Không bao giờ viết logic nghiệp vụ trực tiếp trong các file Widget UI.
|
||||
|
||||
---
|
||||
|
||||
### 🟢 `BUG-006`: `UnicodeEncodeError` khi in Emojis trên Console Windows (CP932/CP1252)
|
||||
|
||||
* **Phân hệ / File**: `scripts/check_imports.py`
|
||||
* **Triệu chứng (Symptom)**:
|
||||
```text
|
||||
Traceback (most recent call last):
|
||||
File "scripts/check_imports.py", line 127, in main
|
||||
print(f"\U0001f6e1\ufe0f Running Clean Architecture Import Guard...")
|
||||
UnicodeEncodeError: 'cp932' codec can't encode character '\U0001f6e1' in position 0: illegal multibyte sequence
|
||||
```
|
||||
* **Nguyên nhân gốc rễ (Root Cause)**:
|
||||
- Trên hệ điều hành Windows sử dụng locale tiếng Nhật (mã trang CP932) hoặc tiếng Anh (CP1252), `sys.stdout` mặc định không hỗ trợ các ký tự Unicode/Emoji ngoài bảng mã, dẫn đến crash khi in log dòng lệnh.
|
||||
* **Giải pháp khắc phục (Resolution)**:
|
||||
- Tự động bọc lại `sys.stdout` và `sys.stderr` bằng `io.TextIOWrapper` với `encoding="utf-8"` và `errors="replace"`.
|
||||
- Thay thế các emoji phức tạp bằng các tag văn bản ASCII chuẩn hóa như `[Clean Arch Guard]`, `[PASS]`, `[FAIL]`.
|
||||
* **Quy tắc phòng ngừa (Prevention Rule - TUYỆT ĐỐI KHÔNG TÁI PHẠM)**:
|
||||
> **Quy tắc**: Mọi script CLI (`scripts/*.py`) phải có cơ chế cấu hình `utf-8` stream wrapper và ưu tiên sử dụng text tags (`[INFO]`, `[WARN]`, `[ERROR]`) thay vì emoji Unicode trực tiếp để đảm bảo chạy mượt mà trên mọi môi trường Windows đa ngôn ngữ.
|
||||
|
||||
---
|
||||
|
||||
### 🟢 `BUG-007`: Xung Đột Tên Thư Mục Trùng Với Standard Library (`platform/` Shadowing `import platform`)
|
||||
|
||||
* **Phân hệ / File**: `platform/` ➔ Chuyển thành `infrastructure/platform/`
|
||||
* **Triệu chứng (Symptom)**:
|
||||
```text
|
||||
INTERNALERROR> File "_pytest/terminal.py", line 853: verinfo = platform.python_version()
|
||||
INTERNALERROR> AttributeError: module 'platform' has no attribute 'python_version'
|
||||
```
|
||||
* **Nguyên nhân gốc rễ (Root Cause)**:
|
||||
- Khi tạo một package ở thư mục gốc có tên trùng với module thư viện chuẩn của Python (`platform`, `email`, `test`, `asyncio`, `logging`), Python trên `sys.path` sẽ ưu tiên import thư mục local thay vì thư viện chuẩn của Python runtime, dẫn đến crash toàn bộ pytest runner và các thư viện bên thứ ba.
|
||||
* **Giải pháp khắc phục (Resolution)**:
|
||||
- Xóa bỏ package `platform/` ở root.
|
||||
- Đưa adapter Qt Scheduler Clock vào đúng vị trí hạ tầng: `infrastructure/platform/qt/`.
|
||||
* **Quy tắc phòng ngừa (Prevention Rule - TUYỆT ĐỐI KHÔNG TÁI PHẠM)**:
|
||||
> **Quy tắc**: Tuyệt đối không đặt tên package/thư mục ở root trùng với tên các module built-in của Python (`platform`, `logging`, `types`, `time`, `io`, `os`, `sys`). Mọi platform adapter phải nằm trong `infrastructure/platform/` hoặc `platform_adapters/`.
|
||||
|
||||
---
|
||||
|
||||
## 📝 MẪU GHI NHẬN BUG MỚI (BUG REPORT TEMPLATE)
|
||||
|
||||
Khi gặp bất kỳ bug mới nào trong quá trình làm việc, hãy sao chép khối mẫu sau và điền vào cuối tài liệu:
|
||||
|
||||
```markdown
|
||||
### 🔴 `BUG-XXX`: [Tóm tắt ngắn gọn tên lỗi]
|
||||
|
||||
* **Phân hệ / File**: `[Đường dẫn file bị lỗi]`
|
||||
* **Triệu chứng (Symptom)**: `[Mô tả hiện tượng lỗi, paste thông báo traceback hoặc kết quả test fail]`
|
||||
* **Nguyên nhân gốc rễ (Root Cause)**: `[Giải thích tại sao lỗi lại xảy ra]`
|
||||
* **Giải pháp khắc phục (Resolution)**: `[Mô tả cách sửa, file DTO/Service tạo mới hoặc cách refactor]`
|
||||
* **Quy tắc phòng ngừa (Prevention Rule - TUYỆT ĐỐI KHÔNG TÁI PHẠM)**:
|
||||
> **Quy tắc**: `[Nguyên tắc kỹ thuật cụ thể để không bao giờ tái phạm lỗi này]`
|
||||
```
|
||||
@@ -0,0 +1,68 @@
|
||||
# Tin nhắn gửi Team Hoa — 25/08/2026
|
||||
|
||||
*Nam (Team Gamma) soạn. Hai việc, không cần trả lời, chỉ cần đọc trước khi
|
||||
bắt đầu R07-T03 và R06.*
|
||||
|
||||
---
|
||||
|
||||
Chào team Hoa,
|
||||
|
||||
Có hai thứ trong nhánh `gamma/refactor` ảnh hưởng trực tiếp tới phần các bạn
|
||||
sắp làm. Gửi trước để khỏi mất thời gian truy lỗi.
|
||||
|
||||
## 1. `platform/` đã đổi tên thành `adapters/` — plan.md ghi tên cũ
|
||||
|
||||
Plan chỉ đích danh `platform/qt/qt_scheduler_clock.py` (R07-T03, dòng 407 và
|
||||
lịch 25/08 ở dòng 229). **Đừng tạo thư mục `platform/`.**
|
||||
|
||||
Lý do: `platform` là tên một module trong thư viện chuẩn của Python. Tạo thư
|
||||
mục `platform/` ở gốc repo là nó che mất module chuẩn khi chạy từ chính thư
|
||||
mục gốc — mà đó là cách toàn bộ script trong `tools/` và `scripts/` đang chạy.
|
||||
Triệu chứng không hề chỉ về đúng chỗ:
|
||||
|
||||
AttributeError: module 'platform' has no attribute 'system'
|
||||
|
||||
Ném ra từ `import keyring`, không liên quan gì tới file bạn vừa tạo.
|
||||
|
||||
Tôi đã mắc đúng lỗi này hôm 21/08. Lúc thử thì đứng ở thư mục cha nên không
|
||||
tái hiện được, tưởng an toàn. Đổi tên thành `adapters/` và thêm
|
||||
`tests/test_no_stdlib_shadow.py` để lần sau đỏ ngay.
|
||||
|
||||
**Việc cần làm**: tạo `adapters/qt/qt_scheduler_clock.py` thay vì
|
||||
`platform/qt/...`. Thư mục `adapters/qt/` đã có sẵn `__init__.py` trên nhánh
|
||||
`gamma/refactor`, kéo về là dùng được.
|
||||
|
||||
## 2. `AtomicJsonFile` vừa vá một lỗi Windows — lấy bản mới trước khi dựng lên
|
||||
|
||||
Plan giao các bạn hai repository ngồi trên `AtomicJsonFile`:
|
||||
|
||||
* `infrastructure/persistence/json/task_repository_impl.py` (R07-T01, dòng 403)
|
||||
* `infrastructure/persistence/json/workspace_repository_impl.py` (R06, dòng 387)
|
||||
|
||||
Và tiêu chí nghiệm thu **A** (dòng 244) bắt mọi thao tác ghi tệp phải đi qua nó.
|
||||
|
||||
Hôm nay tôi bắt được lỗi thật trong đó:
|
||||
|
||||
PermissionError: [WinError 5] Access is denied
|
||||
.dem.json.xxxxxxx.tmp -> dem.json
|
||||
|
||||
`os.replace` trên Windows bị từ chối khi Defender hoặc Search Indexer đang giữ
|
||||
handle lên file vừa tạo — vài chục mili-giây rồi nhả. Đo được: hỏng 1 trong 7
|
||||
lượt chạy 20 lần ghi, tức **khoảng 1 trên 140 lần lưu**. Người dùng thỉnh
|
||||
thoảng bấm Lưu là văng lỗi và không tài nào tái hiện để báo.
|
||||
|
||||
Đã thêm vòng thử lại (commit `9d6a7be`). Nếu các bạn dựng repository trên bản
|
||||
trước đó thì lưu task và lưu workspace cũng hỏng với tần suất y hệt — nhân lên
|
||||
ba nơi ghi file.
|
||||
|
||||
**Việc cần làm**: `git pull` nhánh `gamma/refactor` (hoặc chờ nó vào `main`)
|
||||
trước khi bắt đầu R06/R07-T01.
|
||||
|
||||
## Tiện thể
|
||||
|
||||
`domain/security/tool_policy.py` là bản đề xuất DTO `ToolPolicyGateway` tôi
|
||||
viết hộ cho R05 của các bạn — ba trạng thái ALLOW/DENY/ASK, kèm fake và test
|
||||
contract. Không có gì của Gamma phụ thuộc vào nó, nên các bạn cứ sửa hoặc bỏ
|
||||
thoải mái, không phải giữ ý.
|
||||
|
||||
Nam
|
||||
@@ -0,0 +1 @@
|
||||
"""domain/ — Quy tắc nghiệp vụ thuần. KHÔNG import PySide6, không chạm đĩa/mạng."""
|
||||
@@ -0,0 +1 @@
|
||||
"""Domain agents package: turn requests, agent events, and role definitions."""
|
||||
@@ -0,0 +1,358 @@
|
||||
"""Typed events a turn emits while it runs (R04-T02).
|
||||
|
||||
The runtime currently speaks in bare dicts: ``emit({"type": "tool_result", "id":
|
||||
..., "ok": ...})``. Nothing declares which keys a given type carries, so the
|
||||
only specification is the 130-line ``if/elif`` chain in
|
||||
``ui/chat_panel.py::_on_event`` — and a typo in an emitter surfaces as a widget
|
||||
that silently renders nothing.
|
||||
|
||||
This module makes the vocabulary explicit. Each event is a frozen dataclass with
|
||||
real fields, and each one knows how to serialise itself back to the exact legacy
|
||||
dict the widget already reads (:meth:`AgentEvent.to_legacy_dict`), with
|
||||
:func:`from_legacy_dict` parsing the other way. That two-way bridge is what lets
|
||||
R04 introduce typed events WITHOUT touching the presentation layer — decomposing
|
||||
``_on_event`` into a renderer is R08-T01's job, and forcing both changes into one
|
||||
PR is exactly the "rewrite everything at once" the refactor plan forbids.
|
||||
|
||||
Scope note: this covers the interactive/scheduled **Cowork turn** vocabulary
|
||||
(the ``run_cowork`` path R04 unifies). Co4E's own node events (``node_status``,
|
||||
``stage_text``, ``run_done``) belong to ``Co4EWorkflowService`` in R07-T06 and
|
||||
are deliberately left as dicts here — :func:`from_legacy_dict` returns ``None``
|
||||
for them so a bridge can pass them straight through.
|
||||
|
||||
Layer rules (``docs/architecture/ADR-001-layered-architecture.md``): domain
|
||||
layer, standard library only. No PySide6, no ``core/*`` imports.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, ClassVar, Dict, Iterable, Optional, Tuple
|
||||
|
||||
# Notice levels. "progress" is special-cased by the UI (it retargets the live
|
||||
# thinking indicator instead of adding a bubble), so the vocabulary is pinned
|
||||
# here rather than left to each emitter's string literal.
|
||||
NOTICE_INFO = "info"
|
||||
NOTICE_WARNING = "warning"
|
||||
NOTICE_PROGRESS = "progress"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Value objects shared by several events.
|
||||
# --------------------------------------------------------------------------- #
|
||||
@dataclass(frozen=True)
|
||||
class ToolPreview:
|
||||
"""The human-readable preview of a proposed tool call.
|
||||
|
||||
Mirrors ``core/tools.py::describe_action``'s return shape exactly (three
|
||||
string keys, nothing else), so wrapping it in a type is lossless. ``kind``
|
||||
drives which bubble the UI renders: "diff" -> coloured before/after,
|
||||
"command" -> terminal block, "info" -> plain text.
|
||||
"""
|
||||
|
||||
kind: str = "info"
|
||||
title: str = ""
|
||||
text: str = ""
|
||||
|
||||
def to_dict(self) -> Dict[str, str]:
|
||||
return {"kind": self.kind, "title": self.title, "text": self.text}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, raw: Any) -> Optional["ToolPreview"]:
|
||||
"""Parse a legacy preview dict; ``None`` when there was none.
|
||||
|
||||
A non-dict value degrades to ``None`` rather than raising: a malformed
|
||||
preview must cost the user a nicer bubble, never the whole turn.
|
||||
"""
|
||||
if not isinstance(raw, dict) or not raw:
|
||||
return None
|
||||
return cls(kind=str(raw.get("kind", "info")), title=str(raw.get("title", "")),
|
||||
text=str(raw.get("text", "")))
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PlanStep:
|
||||
"""One entry of the agent's ``update_plan`` checklist.
|
||||
|
||||
``status`` is kept a plain string on purpose: ``core/plan.py`` already owns
|
||||
validation (clamping anything unknown to "pending" against
|
||||
pending/running/done/error), and duplicating that vocabulary here would give
|
||||
the app two sources of truth to drift apart.
|
||||
"""
|
||||
|
||||
title: str
|
||||
status: str = "pending"
|
||||
|
||||
def to_dict(self) -> Dict[str, str]:
|
||||
return {"title": self.title, "status": self.status}
|
||||
|
||||
|
||||
def _as_str_tuple(values: Iterable[Any]) -> Tuple[str, ...]:
|
||||
"""Freeze an iterable of paths into a tuple of strings.
|
||||
|
||||
Emitters hand us live lists (``record["outputs"]``, ``_cleanup``'s result);
|
||||
copying decouples the event from later mutation of that list.
|
||||
"""
|
||||
return tuple(str(v) for v in (values or ()))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Base class.
|
||||
# --------------------------------------------------------------------------- #
|
||||
class AgentEvent:
|
||||
"""Base for every turn event.
|
||||
|
||||
Not a dataclass itself (it holds no data) — subclasses are the frozen
|
||||
dataclasses. ``EVENT_TYPE`` is the legacy wire name, which stays the single
|
||||
identifier shared between the typed world and the dict world.
|
||||
"""
|
||||
|
||||
EVENT_TYPE: ClassVar[str] = ""
|
||||
|
||||
def _payload(self) -> Dict[str, Any]:
|
||||
"""Type-specific keys of the legacy dict (without ``type``)."""
|
||||
return {}
|
||||
|
||||
def to_legacy_dict(self) -> Dict[str, Any]:
|
||||
"""The exact dict shape ``ui/chat_panel.py::_on_event`` dispatches on."""
|
||||
return {"type": self.EVENT_TYPE, **self._payload()}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Streaming events.
|
||||
# --------------------------------------------------------------------------- #
|
||||
@dataclass(frozen=True)
|
||||
class TextChunkEvent(AgentEvent):
|
||||
"""A fragment of the assistant's visible answer."""
|
||||
|
||||
EVENT_TYPE: ClassVar[str] = "text"
|
||||
delta: str = ""
|
||||
|
||||
def _payload(self) -> Dict[str, Any]:
|
||||
return {"delta": self.delta}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ReasoningChunkEvent(AgentEvent):
|
||||
"""A fragment of a reasoning model's thinking, shown in a collapsed box."""
|
||||
|
||||
EVENT_TYPE: ClassVar[str] = "reasoning"
|
||||
delta: str = ""
|
||||
|
||||
def _payload(self) -> Dict[str, Any]:
|
||||
return {"delta": self.delta}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AssistantMessageCompletedEvent(AgentEvent):
|
||||
"""One assistant message finished streaming.
|
||||
|
||||
Emitted once per provider call, so a tool-using turn produces SEVERAL of
|
||||
these — it marks an autosave point, not the end of the turn. The end of the
|
||||
turn is :class:`TurnCompletedEvent`.
|
||||
"""
|
||||
|
||||
EVENT_TYPE: ClassVar[str] = "assistant_done"
|
||||
content: str = ""
|
||||
|
||||
def _payload(self) -> Dict[str, Any]:
|
||||
return {"content": self.content}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Tool-call lifecycle.
|
||||
# --------------------------------------------------------------------------- #
|
||||
@dataclass(frozen=True)
|
||||
class ToolCallStartedEvent(AgentEvent):
|
||||
"""A tool call is about to run (after any security/permission gate).
|
||||
|
||||
Field names are the typed ones (``call_id``, ``arguments``); the legacy keys
|
||||
``id``/``args`` are produced only at the serialisation boundary, so new code
|
||||
never has to shadow the ``id`` builtin.
|
||||
"""
|
||||
|
||||
EVENT_TYPE: ClassVar[str] = "tool_proposed"
|
||||
call_id: str = ""
|
||||
name: str = ""
|
||||
arguments: Dict[str, Any] = field(default_factory=dict)
|
||||
preview: Optional[ToolPreview] = None
|
||||
|
||||
def _payload(self) -> Dict[str, Any]:
|
||||
payload: Dict[str, Any] = {"id": self.call_id, "name": self.name,
|
||||
"args": dict(self.arguments)}
|
||||
# Omitted rather than sent as None: the widget does
|
||||
# ``preview = ev.get("preview") or {}`` and an absent key is the shape it
|
||||
# already handles for tools without a preview.
|
||||
if self.preview is not None:
|
||||
payload["preview"] = self.preview.to_dict()
|
||||
return payload
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ToolOutputChunkEvent(AgentEvent):
|
||||
"""Live stdout/stderr from a running command, appended to its step bubble."""
|
||||
|
||||
EVENT_TYPE: ClassVar[str] = "tool_output"
|
||||
call_id: str = ""
|
||||
name: str = ""
|
||||
delta: str = ""
|
||||
|
||||
def _payload(self) -> Dict[str, Any]:
|
||||
return {"id": self.call_id, "name": self.name, "delta": self.delta}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ToolCallFinishedEvent(AgentEvent):
|
||||
"""A tool call returned. ``path``/``produced`` name files it created."""
|
||||
|
||||
EVENT_TYPE: ClassVar[str] = "tool_result"
|
||||
call_id: str = ""
|
||||
name: str = ""
|
||||
ok: bool = False
|
||||
output: str = ""
|
||||
path: str = "" # the single file this call wrote, if any
|
||||
produced: Tuple[str, ...] = () # extra deliverables a command produced
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
# Callers pass a live list; freeze it so the event cannot change later.
|
||||
object.__setattr__(self, "produced", _as_str_tuple(self.produced))
|
||||
|
||||
def _payload(self) -> Dict[str, Any]:
|
||||
payload: Dict[str, Any] = {"id": self.call_id, "name": self.name,
|
||||
"ok": self.ok, "output": self.output}
|
||||
# Both keys stay ABSENT when empty, matching what chat_agent emits today:
|
||||
# downstream code tests them with ``ev.get(...)`` truthiness and iterates
|
||||
# ``ev.get("produced", [])``, so adding empty values would be a change.
|
||||
if self.path:
|
||||
payload["path"] = self.path
|
||||
if self.produced:
|
||||
payload["produced"] = list(self.produced)
|
||||
return payload
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Side-channel events (plan, notices, output folder).
|
||||
# --------------------------------------------------------------------------- #
|
||||
@dataclass(frozen=True)
|
||||
class PlanUpdatedEvent(AgentEvent):
|
||||
"""The agent published a new version of its step checklist (full list)."""
|
||||
|
||||
EVENT_TYPE: ClassVar[str] = "plan_set"
|
||||
steps: Tuple[PlanStep, ...] = ()
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
object.__setattr__(self, "steps", tuple(self.steps or ()))
|
||||
|
||||
def _payload(self) -> Dict[str, Any]:
|
||||
return {"steps": [s.to_dict() for s in self.steps]}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class NoticeEvent(AgentEvent):
|
||||
"""An aside outside the model's own answer.
|
||||
|
||||
Three sources today: context auto-compaction (info), a blocked
|
||||
security check (warning), and attachment reading progress (progress).
|
||||
"""
|
||||
|
||||
EVENT_TYPE: ClassVar[str] = "notice"
|
||||
text: str = ""
|
||||
level: str = NOTICE_INFO
|
||||
|
||||
def _payload(self) -> Dict[str, Any]:
|
||||
return {"level": self.level, "text": self.text}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OutputsAddedEvent(AgentEvent):
|
||||
"""Deliverables appeared in the turn's output folder."""
|
||||
|
||||
EVENT_TYPE: ClassVar[str] = "outputs_added"
|
||||
paths: Tuple[str, ...] = ()
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
object.__setattr__(self, "paths", _as_str_tuple(self.paths))
|
||||
|
||||
def _payload(self) -> Dict[str, Any]:
|
||||
return {"paths": list(self.paths)}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class OutputsRemovedEvent(AgentEvent):
|
||||
"""Intermediate/generator files were cleaned up — drop them from Output."""
|
||||
|
||||
EVENT_TYPE: ClassVar[str] = "outputs_removed"
|
||||
paths: Tuple[str, ...] = ()
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
object.__setattr__(self, "paths", _as_str_tuple(self.paths))
|
||||
|
||||
def _payload(self) -> Dict[str, Any]:
|
||||
return {"paths": list(self.paths)}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class HistoryReadyEvent(AgentEvent):
|
||||
"""The turn's conversation now exists on disk and can be opened.
|
||||
|
||||
Emitted by the unattended (Schedule Task) path so the scheduler refreshes
|
||||
History only once the session is really there.
|
||||
"""
|
||||
|
||||
EVENT_TYPE: ClassVar[str] = "history_ready"
|
||||
session_id: str = ""
|
||||
|
||||
def _payload(self) -> Dict[str, Any]:
|
||||
return {"session_id": self.session_id}
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------- #
|
||||
# Turn-level events introduced by R04 (no legacy consumer yet).
|
||||
# --------------------------------------------------------------------------- #
|
||||
@dataclass(frozen=True)
|
||||
class TurnCompletedEvent(AgentEvent):
|
||||
"""The whole turn ended — exactly once per turn.
|
||||
|
||||
Nothing consumes ``"turn_completed"`` yet: the widget's ``if/elif`` chain
|
||||
simply has no branch for it, so emitting it is inert until R08 wires a
|
||||
renderer. It exists now because the state it carries (was the turn
|
||||
cancelled? did it hit the step ceiling?) is currently reconstructed by the
|
||||
UI from side effects rather than being told to it.
|
||||
"""
|
||||
|
||||
EVENT_TYPE: ClassVar[str] = "turn_completed"
|
||||
final_text: str = ""
|
||||
steps_used: int = 0
|
||||
cancelled: bool = False
|
||||
budget_exhausted: bool = False # stopped at effective_max_steps
|
||||
|
||||
def _payload(self) -> Dict[str, Any]:
|
||||
return {"final_text": self.final_text, "steps_used": self.steps_used,
|
||||
"cancelled": self.cancelled, "budget_exhausted": self.budget_exhausted}
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ErrorEvent(AgentEvent):
|
||||
"""The turn hit an error.
|
||||
|
||||
``recoverable`` separates "this turn is over" from "something failed but the
|
||||
loop carried on" — a distinction the current code loses, because both end up
|
||||
as a bare ``except Exception`` plus a text bubble.
|
||||
"""
|
||||
|
||||
EVENT_TYPE: ClassVar[str] = "error"
|
||||
message: str = ""
|
||||
recoverable: bool = False
|
||||
|
||||
def _payload(self) -> Dict[str, Any]:
|
||||
return {"message": self.message, "recoverable": self.recoverable}
|
||||
|
||||
|
||||
__all__ = [
|
||||
"NOTICE_INFO", "NOTICE_WARNING", "NOTICE_PROGRESS",
|
||||
"AgentEvent", "ToolPreview", "PlanStep",
|
||||
"TextChunkEvent", "ReasoningChunkEvent", "AssistantMessageCompletedEvent",
|
||||
"ToolCallStartedEvent", "ToolOutputChunkEvent", "ToolCallFinishedEvent",
|
||||
"PlanUpdatedEvent", "NoticeEvent", "OutputsAddedEvent", "OutputsRemovedEvent",
|
||||
"HistoryReadyEvent", "TurnCompletedEvent", "ErrorEvent",
|
||||
]
|
||||
@@ -0,0 +1,123 @@
|
||||
"""Legacy dict -> typed :mod:`agent_event` translation (R04-T02).
|
||||
|
||||
Kept in its own module for two reasons. It is a **temporary compatibility
|
||||
shim**: once R08-T01 turns ``ui/chat_panel.py::_on_event`` into an event
|
||||
renderer that consumes typed events directly, nothing needs to parse dicts any
|
||||
more and this whole file gets deleted — a deletion that stays trivial only while
|
||||
it is isolated. And it keeps ``agent_event.py`` inside the 400-LOC limit the
|
||||
architecture rules impose, without diluting either file's single job: one
|
||||
declares the vocabulary, the other bridges it to the old wire format.
|
||||
|
||||
Serialisation the other way lives on the events themselves
|
||||
(``AgentEvent.to_legacy_dict``), because an event has to be emittable without
|
||||
anyone importing a codec.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List, Optional, Tuple
|
||||
|
||||
from .agent_event import (
|
||||
AgentEvent,
|
||||
AssistantMessageCompletedEvent,
|
||||
ErrorEvent,
|
||||
HistoryReadyEvent,
|
||||
NOTICE_INFO,
|
||||
NoticeEvent,
|
||||
OutputsAddedEvent,
|
||||
OutputsRemovedEvent,
|
||||
PlanStep,
|
||||
PlanUpdatedEvent,
|
||||
ReasoningChunkEvent,
|
||||
TextChunkEvent,
|
||||
ToolCallFinishedEvent,
|
||||
ToolCallStartedEvent,
|
||||
ToolOutputChunkEvent,
|
||||
ToolPreview,
|
||||
TurnCompletedEvent,
|
||||
)
|
||||
|
||||
|
||||
def _plan_steps_from_legacy(raw: Any) -> Tuple[PlanStep, ...]:
|
||||
"""Parse the legacy ``steps`` list, dropping anything unusable.
|
||||
|
||||
A step with no title cannot be rendered or ticked off, so it is discarded
|
||||
instead of becoming a blank row in the Plan panel.
|
||||
"""
|
||||
if not isinstance(raw, list):
|
||||
return ()
|
||||
steps: List[PlanStep] = []
|
||||
for item in raw:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
title = str(item.get("title", "")).strip()
|
||||
if not title:
|
||||
continue
|
||||
steps.append(PlanStep(title=title, status=str(item.get("status", "pending"))))
|
||||
return tuple(steps)
|
||||
|
||||
|
||||
def _parse_tool_started(raw: Dict[str, Any]) -> ToolCallStartedEvent:
|
||||
"""Rebuild a ``tool_proposed`` event, mapping ``id``/``args`` to typed names."""
|
||||
args = raw.get("args")
|
||||
return ToolCallStartedEvent(
|
||||
call_id=str(raw.get("id", "")), name=str(raw.get("name", "")),
|
||||
arguments=dict(args) if isinstance(args, dict) else {},
|
||||
preview=ToolPreview.from_dict(raw.get("preview")),
|
||||
)
|
||||
|
||||
|
||||
def _parse_tool_finished(raw: Dict[str, Any]) -> ToolCallFinishedEvent:
|
||||
"""Rebuild a ``tool_result`` event; the optional file keys may be absent."""
|
||||
return ToolCallFinishedEvent(
|
||||
call_id=str(raw.get("id", "")), name=str(raw.get("name", "")),
|
||||
ok=bool(raw.get("ok", False)), output=str(raw.get("output", "")),
|
||||
path=str(raw.get("path", "") or ""), produced=raw.get("produced") or (),
|
||||
)
|
||||
|
||||
|
||||
# One parser per wire name. A table (rather than an if/elif chain) keeps adding
|
||||
# an event a single-line change and makes the supported set introspectable.
|
||||
_PARSERS = {
|
||||
TextChunkEvent.EVENT_TYPE: lambda raw: TextChunkEvent(delta=str(raw.get("delta", ""))),
|
||||
ReasoningChunkEvent.EVENT_TYPE: lambda raw: ReasoningChunkEvent(
|
||||
delta=str(raw.get("delta", ""))),
|
||||
AssistantMessageCompletedEvent.EVENT_TYPE: lambda raw: AssistantMessageCompletedEvent(
|
||||
content=str(raw.get("content", ""))),
|
||||
ToolCallStartedEvent.EVENT_TYPE: _parse_tool_started,
|
||||
ToolOutputChunkEvent.EVENT_TYPE: lambda raw: ToolOutputChunkEvent(
|
||||
call_id=str(raw.get("id", "")), name=str(raw.get("name", "")),
|
||||
delta=str(raw.get("delta", ""))),
|
||||
ToolCallFinishedEvent.EVENT_TYPE: _parse_tool_finished,
|
||||
PlanUpdatedEvent.EVENT_TYPE: lambda raw: PlanUpdatedEvent(
|
||||
steps=_plan_steps_from_legacy(raw.get("steps"))),
|
||||
NoticeEvent.EVENT_TYPE: lambda raw: NoticeEvent(
|
||||
text=str(raw.get("text", "")), level=str(raw.get("level", NOTICE_INFO))),
|
||||
OutputsAddedEvent.EVENT_TYPE: lambda raw: OutputsAddedEvent(paths=raw.get("paths") or ()),
|
||||
OutputsRemovedEvent.EVENT_TYPE: lambda raw: OutputsRemovedEvent(paths=raw.get("paths") or ()),
|
||||
HistoryReadyEvent.EVENT_TYPE: lambda raw: HistoryReadyEvent(
|
||||
session_id=str(raw.get("session_id", ""))),
|
||||
TurnCompletedEvent.EVENT_TYPE: lambda raw: TurnCompletedEvent(
|
||||
final_text=str(raw.get("final_text", "")), steps_used=int(raw.get("steps_used", 0) or 0),
|
||||
cancelled=bool(raw.get("cancelled", False)),
|
||||
budget_exhausted=bool(raw.get("budget_exhausted", False))),
|
||||
ErrorEvent.EVENT_TYPE: lambda raw: ErrorEvent(
|
||||
message=str(raw.get("message", "")), recoverable=bool(raw.get("recoverable", False))),
|
||||
}
|
||||
|
||||
|
||||
def from_legacy_dict(payload: Any) -> Optional[AgentEvent]:
|
||||
"""Parse an emitted dict into a typed event, or ``None`` if it isn't ours.
|
||||
|
||||
``None`` (rather than an exception) is the contract that makes incremental
|
||||
adoption possible: a bridge sitting between the runtime and the widget can
|
||||
type the events it recognises and forward everything else — Co4E's node
|
||||
events, or anything a future emitter adds — completely untouched.
|
||||
"""
|
||||
if not isinstance(payload, dict):
|
||||
return None
|
||||
parser = _PARSERS.get(str(payload.get("type", "")))
|
||||
return parser(payload) if parser is not None else None
|
||||
|
||||
|
||||
__all__ = ["from_legacy_dict"]
|
||||
@@ -0,0 +1,86 @@
|
||||
"""What one finished turn produced (R04-T03).
|
||||
|
||||
The outcome of a turn is currently spread over three shapes: ``run_cowork``
|
||||
returns the mutated message list, ``task_executors._run_agent`` returns a
|
||||
``(answer_text, timed_out, incomplete_reason)`` tuple, and the UI reconstructs
|
||||
the rest (did it get cancelled? did it hit the ceiling?) from side effects. Each
|
||||
caller therefore knows a slightly different amount about the same turn.
|
||||
|
||||
:class:`AgentResult` is the single answer. Frozen, like the request that started
|
||||
the turn, so a result cannot be edited into disagreeing with what actually
|
||||
happened.
|
||||
|
||||
Layer rules (``docs/architecture/ADR-001-layered-architecture.md``): domain
|
||||
layer — standard library plus sibling domain types only.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Dict, Tuple
|
||||
|
||||
from .agent_event import PlanStep, TurnCompletedEvent
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AgentResult:
|
||||
"""The outcome of one conversation turn."""
|
||||
|
||||
# The conversation AFTER the turn (system prompt, history, the new user
|
||||
# message, every assistant reply and tool result).
|
||||
messages: Tuple[Dict[str, Any], ...] = ()
|
||||
steps_used: int = 0 # provider calls this turn consumed
|
||||
cancelled: bool = False # the user pressed Stop
|
||||
budget_exhausted: bool = False # stopped at effective_max_steps
|
||||
# The agent's final checklist, so a caller can ask "did it really finish?"
|
||||
# (``core/plan.py::plan_incomplete_reason``) without replaying the events.
|
||||
plan_steps: Tuple[PlanStep, ...] = ()
|
||||
# Non-empty when the turn ended on a failure. A string rather than the
|
||||
# exception: the domain layer must not depend on where the error came from,
|
||||
# and the message is what every consumer (bubble, error.txt, audit) shows.
|
||||
error: str = ""
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Freeze the collections the runtime hands over.
|
||||
|
||||
Both arrive as live lists that the caller keeps appending to after the
|
||||
turn (the UI merges messages back into its own history), so copying here
|
||||
is what keeps a result a record rather than a moving target.
|
||||
"""
|
||||
object.__setattr__(self, "messages", tuple(self.messages or ()))
|
||||
object.__setattr__(self, "plan_steps", tuple(self.plan_steps or ()))
|
||||
|
||||
@property
|
||||
def final_text(self) -> str:
|
||||
"""The answer to show the user.
|
||||
|
||||
Scans backwards for the last assistant message with real content, which
|
||||
is not the same as ``messages[-1]``: a turn that was cancelled or that
|
||||
ran out of steps mid-loop ends on a tool message, and a reasoning-only
|
||||
reply leaves a blank assistant message behind. Same rule as
|
||||
``core/task_executors.py::_last_assistant_text``, which this replaces.
|
||||
"""
|
||||
for message in reversed(self.messages):
|
||||
if message.get("role") == "assistant" and (message.get("content") or "").strip():
|
||||
return str(message["content"])
|
||||
return ""
|
||||
|
||||
@property
|
||||
def ok(self) -> bool:
|
||||
"""Whether the turn ran to a normal end.
|
||||
|
||||
Hitting the step ceiling still counts as ok: the agent did work and
|
||||
produced an answer, it just was not allowed to keep going — which the
|
||||
transcript says in its own note rather than by failing the turn.
|
||||
"""
|
||||
return not self.error and not self.cancelled
|
||||
|
||||
def to_turn_completed_event(self) -> TurnCompletedEvent:
|
||||
"""The end-of-turn event carrying this outcome to subscribers."""
|
||||
return TurnCompletedEvent(
|
||||
final_text=self.final_text, steps_used=self.steps_used,
|
||||
cancelled=self.cancelled, budget_exhausted=self.budget_exhausted,
|
||||
)
|
||||
|
||||
|
||||
__all__ = ["AgentResult"]
|
||||
@@ -0,0 +1,222 @@
|
||||
"""The immutable snapshot of ONE chat turn (R04-T01).
|
||||
|
||||
Today a turn's inputs live in a closure plus a 15-key ``ctx`` dict built inside
|
||||
``ui/chat_panel.py::_start_turn``, and the worker thread reads the widget back
|
||||
(``self._model``, ``self.title``, ``self.project_id``) while it runs. That is
|
||||
the mechanism behind the whole class of "I changed the model mid-answer and the
|
||||
running turn behaved oddly" reports: the turn has no snapshot of its own, so
|
||||
every later click on the UI is visible to work already in flight.
|
||||
|
||||
:class:`ConversationExecutionRequest` is that missing snapshot. Everything the
|
||||
runtime needs for one turn is captured once, on the UI thread, at submit time,
|
||||
and then handed to code that runs on a worker thread. Frozen, so no caller —
|
||||
widget or service — can retroactively change a decision the turn already acted
|
||||
on.
|
||||
|
||||
Layer rules (see ``docs/architecture/ADR-001-layered-architecture.md``): this is
|
||||
the domain layer, so standard library only. No PySide6, no ``requests``, no
|
||||
filesystem access, and deliberately no import of ``core/*`` — a request only
|
||||
*describes* a turn; running it is the application layer's job
|
||||
(``application/conversations/``).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field, replace
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
|
||||
# Separator between an instruction prefix (a ``/skill`` block, an ``/agent``
|
||||
# persona) and the user's own request. Kept as a constant because the prefix is
|
||||
# assembled in the presentation layer while the body is only known later on the
|
||||
# worker thread — both halves must agree on the exact separator or the model
|
||||
# sees a different prompt shape than it did before this refactor.
|
||||
PREFIX_SEPARATOR = "\n\n---\n\n"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ConversationExecutionRequest:
|
||||
"""Everything needed to execute one conversation turn.
|
||||
|
||||
Frozen for the reason above; use :meth:`with_model` / :meth:`with_output_dir`
|
||||
to derive an adjusted copy rather than mutating one another thread may be
|
||||
reading.
|
||||
|
||||
Note on depth: ``messages`` is a *shallow* snapshot (a tuple holding the
|
||||
same message dicts the caller passed). That matches the existing
|
||||
``snapshot = list(self.messages)`` semantics in ``_start_turn`` exactly —
|
||||
the turn is protected from the history list being appended to or replaced,
|
||||
which is what actually happens between turns. Making it deep would silently
|
||||
change how ``_finalize_turn`` merges the turn's messages back, so the
|
||||
stronger guarantee is left to R04-T03 where that merge moves.
|
||||
"""
|
||||
|
||||
# -- identity ------------------------------------------------------- #
|
||||
turn_id: str # unique within a session ("t1", "t2", ...)
|
||||
session_id: str # the conversation this turn belongs to
|
||||
surface: str = "cowork" # routing/mode key: "cowork" | "co4e" | "ai_edit"
|
||||
project_id: str = "" # workspace the turn is confined to
|
||||
title: str = "" # conversation title; also names saved files
|
||||
|
||||
# -- what the user asked -------------------------------------------- #
|
||||
# The typed request, already stripped of any ``/skill`` or ``/agent``
|
||||
# directive (those become ``instruction_prefix``).
|
||||
prompt: str = ""
|
||||
instruction_prefix: str = "" # skill rules + agent persona for this turn
|
||||
# Prepended when the model/agent was switched mid-conversation, asking the
|
||||
# model to re-check the previous step before continuing. Invisible in the
|
||||
# chat bubble — it only travels in the payload sent to the provider.
|
||||
review_note: str = ""
|
||||
# Attachment PATHS, not their text: extracting a .docx can pip-install a
|
||||
# parser or shell out to LibreOffice, which must not run on the UI thread.
|
||||
# The runtime reads them later and passes the result to :meth:`user_content`.
|
||||
attachments: Tuple[str, ...] = ()
|
||||
# Conversation history as of submit time; the new user message is NOT part
|
||||
# of it (the runtime appends it once the body is composed).
|
||||
messages: Tuple[Dict[str, Any], ...] = ()
|
||||
|
||||
# -- which model answers -------------------------------------------- #
|
||||
# Already resolved upstream: an Admin-agent pin, the tab's own picker, or a
|
||||
# routing override published by ``RoutingApplicationService`` (R03). The
|
||||
# runtime does not re-decide, so a switch cannot land mid-turn.
|
||||
provider_id: str = ""
|
||||
model: str = "" # "" = the provider's configured default
|
||||
|
||||
# -- standing instructions ------------------------------------------ #
|
||||
project_context: str = "" # Claude-Projects-style shared instructions
|
||||
session_notes: str = "" # e.g. files this conversation already produced
|
||||
|
||||
# -- tool scope and turn limits -------------------------------------- #
|
||||
# None = every enabled built-in tool. An explicit (possibly empty) tuple
|
||||
# restricts the ADVERTISED tools, which is how a "read-only" step is made
|
||||
# literally unable to write.
|
||||
allowed_tools: Optional[Tuple[str, ...]] = None
|
||||
max_steps: int = 30 # interactive cap
|
||||
completion_max_steps: int = 200 # runaway ceiling for run-to-completion work
|
||||
run_to_completion: bool = False # Co4E flow steps need the higher ceiling
|
||||
enforce_rules: bool = True # False for sandboxed Co4E runs
|
||||
gate_mode: str = "auto" # "confirm" -> ask before run_command/install
|
||||
agent_role: str = "cowork" # audit-log attribution ("cowork" | "task" | ...)
|
||||
|
||||
# -- where its files go ---------------------------------------------- #
|
||||
output_dir: Optional[Path] = None # this turn's isolated sandbox
|
||||
home_output_root: Optional[Path] = None # conversation Output root to promote into
|
||||
|
||||
# -- unattended execution (Schedule Task) ----------------------------- #
|
||||
unattended: bool = False # no human watching; plan tracking is enforced
|
||||
timeout_sec: Optional[int] = None # None = no wall-clock limit
|
||||
|
||||
# Escape hatch for surface-specific data a future task needs to thread
|
||||
# through without another schema change (same role as
|
||||
# ``ProviderDescriptor.extras``).
|
||||
extras: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
# -- validation / normalisation --------------------------------------- #
|
||||
def __post_init__(self) -> None:
|
||||
"""Reject unusable requests and freeze the mutable inputs.
|
||||
|
||||
Validation lives here (not at the call site) so a request that exists is
|
||||
always safe to key by: the audit log, the History autosave and the
|
||||
per-turn output folder are all named from ``session_id``/``turn_id``.
|
||||
|
||||
Normalisation matters just as much: the caller hands us the composer's
|
||||
own attachment LIST and the live history LIST, and both get cleared or
|
||||
appended to for the next turn. Copying them into tuples here is what
|
||||
actually makes the snapshot a snapshot. ``object.__setattr__`` is the
|
||||
standard way to do this in a frozen dataclass.
|
||||
"""
|
||||
if not (self.turn_id or "").strip():
|
||||
raise ValueError("ConversationExecutionRequest.turn_id must not be empty")
|
||||
if not (self.session_id or "").strip():
|
||||
raise ValueError("ConversationExecutionRequest.session_id must not be empty")
|
||||
|
||||
object.__setattr__(self, "attachments", tuple(self.attachments or ()))
|
||||
object.__setattr__(self, "messages", tuple(self.messages or ()))
|
||||
# None must survive: it means "no restriction", while an empty tuple
|
||||
# means "deny every built-in tool" — two very different turns.
|
||||
if self.allowed_tools is not None:
|
||||
object.__setattr__(self, "allowed_tools", tuple(self.allowed_tools))
|
||||
# Accept str paths so a call site holding a config value does not have to
|
||||
# wrap it; everything downstream can then assume Path.
|
||||
for name in ("output_dir", "home_output_root"):
|
||||
value = getattr(self, name)
|
||||
if value is not None and not isinstance(value, Path):
|
||||
object.__setattr__(self, name, Path(value))
|
||||
|
||||
# -- derived turn policy ---------------------------------------------- #
|
||||
@property
|
||||
def has_prompt(self) -> bool:
|
||||
"""Whether the user actually typed something (an attachment-only turn
|
||||
legitimately has none). Mirrors ``RoutingRequest.has_prompt`` so both
|
||||
DTOs answer the "is there anything to work with?" question the same way.
|
||||
"""
|
||||
return bool((self.prompt or "").strip())
|
||||
|
||||
@property
|
||||
def effective_max_steps(self) -> int:
|
||||
"""The tool-use budget for this turn.
|
||||
|
||||
Run-to-completion work (a Co4E flow step whose single instruction may
|
||||
need many tool calls) gets the higher ceiling; interactive chat keeps the
|
||||
tight cap. Either way the turn still ends the moment the model stops
|
||||
calling tools — this is only the runaway limit.
|
||||
"""
|
||||
return self.completion_max_steps if self.run_to_completion else self.max_steps
|
||||
|
||||
@property
|
||||
def requires_permission_gate(self) -> bool:
|
||||
"""Whether ``run_command``/``install_package`` must be approved first.
|
||||
|
||||
Resolved by the caller (per-workspace Auto-run override, else the global
|
||||
"confirm before running commands" setting) and frozen here, so toggling
|
||||
the setting mid-turn cannot change the rules the turn started under.
|
||||
"""
|
||||
return self.gate_mode == "confirm"
|
||||
|
||||
# -- prompt composition ------------------------------------------------ #
|
||||
def user_content(self, body: str = "") -> str:
|
||||
"""The exact ``content`` to send as this turn's user message.
|
||||
|
||||
``body`` is the request text AFTER attachment extraction, which happens
|
||||
on the worker thread — hence a method taking it as an argument rather
|
||||
than a stored field. The assembly order reproduces the closure in
|
||||
``_start_turn`` byte for byte, because changing what a model receives is
|
||||
a behaviour change, not a refactor:
|
||||
|
||||
1. session notes are appended after the body;
|
||||
2. the instruction prefix goes in front, behind a fixed separator;
|
||||
3. the model-switch review note goes ahead of everything.
|
||||
"""
|
||||
content = body or ""
|
||||
notes = self.session_notes or ""
|
||||
if notes:
|
||||
# Guard the empty-body case (attachment-only turn) so the payload
|
||||
# never opens with a stray blank line.
|
||||
content = f"{content}\n\n{notes}" if content else notes
|
||||
prefix = self.instruction_prefix or ""
|
||||
if prefix:
|
||||
content = f"{prefix}{PREFIX_SEPARATOR}{content}"
|
||||
review = self.review_note or ""
|
||||
if review:
|
||||
content = f"{review}\n\n{content}"
|
||||
return content
|
||||
|
||||
# -- derivation --------------------------------------------------------- #
|
||||
def with_model(self, provider_id: str = "", model: str = "") -> "ConversationExecutionRequest":
|
||||
"""A copy pinned to another provider/model.
|
||||
|
||||
Needed when a decision lands between building the request and running it
|
||||
(a routing override, an Admin-agent pin). Deriving a new request keeps
|
||||
the "one turn, one immutable snapshot" rule intact instead of patching a
|
||||
request another thread may already hold.
|
||||
"""
|
||||
return replace(self, provider_id=provider_id or self.provider_id,
|
||||
model=model or self.model)
|
||||
|
||||
def with_output_dir(self, output_dir) -> "ConversationExecutionRequest":
|
||||
"""A copy writing into a different sandbox — used when the caller only
|
||||
learns the per-turn folder after the request is assembled."""
|
||||
return replace(self, output_dir=output_dir)
|
||||
|
||||
|
||||
__all__ = ["PREFIX_SEPARATOR", "ConversationExecutionRequest"]
|
||||
@@ -0,0 +1 @@
|
||||
"""Domain models package: provider descriptors, model pricing, and routing metadata."""
|
||||
@@ -0,0 +1,196 @@
|
||||
"""Provider catalog metadata — the domain-layer description of ONE LLM provider.
|
||||
|
||||
Before R03 the answer to "which providers exist, what do they cost, what can
|
||||
they do?" was spread over three places: the class table in
|
||||
``providers/factory.py``, the hand-maintained pricing table in
|
||||
``core/routing/metadata.py`` and a handful of ``if provider == "anthropic"``
|
||||
branches in the UI. :class:`ProviderDescriptor` is the single declarative
|
||||
record those call sites now read from.
|
||||
|
||||
Layer rules (see ``docs/architecture/ADR-001-layered-architecture.md``): this
|
||||
module is 100% pure Python — no PySide6, no ``requests``, no filesystem, and no
|
||||
import of the concrete ``providers/*`` adapters. It only *describes* a provider;
|
||||
constructing one is the infrastructure layer's job
|
||||
(``infrastructure/providers/provider_registry.py``).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field, replace
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, Optional, Tuple
|
||||
|
||||
|
||||
class AuthKind(str, Enum):
|
||||
"""How a provider authenticates, so Settings/onboarding can ask for the
|
||||
right thing instead of hard-coding per-provider form fields.
|
||||
|
||||
Inherits ``str`` so a descriptor round-trips through JSON unchanged (the
|
||||
value is written as a plain string), matching how the routing models in
|
||||
``core/routing/models.py`` already serialize their enums.
|
||||
"""
|
||||
|
||||
NONE = "none" # local runtimes (Ollama) — nothing to supply
|
||||
API_KEY = "api_key" # bearer/x-api-key style secret
|
||||
OAUTH_TOKEN = "oauth" # token minted by an external login flow (Copilot)
|
||||
|
||||
|
||||
class WireProtocol(str, Enum):
|
||||
"""The on-the-wire dialect a provider speaks.
|
||||
|
||||
Several *distinct* providers share one protocol (Ollama, Codex, GitHub
|
||||
Copilot and generic gateways are all OpenAI Chat Completions), which is
|
||||
exactly why protocol is a separate field from the provider id: the registry
|
||||
picks the adapter class from the protocol, while everything user-facing
|
||||
keys off the id.
|
||||
"""
|
||||
|
||||
OPENAI_COMPAT = "openai_compat"
|
||||
ANTHROPIC = "anthropic"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ProviderDescriptor:
|
||||
"""Immutable metadata for one provider the app can route work to.
|
||||
|
||||
Frozen because descriptors are shared process-wide by the registry, the
|
||||
routing service and (eventually) the Settings screen; making them read-only
|
||||
removes any chance one caller mutates the catalog another caller is
|
||||
iterating. Use :meth:`with_models` to derive an updated copy instead.
|
||||
|
||||
Unknown pricing/context values stay ``None`` rather than being guessed —
|
||||
the routing scorer needs to distinguish "free" from "we don't know", the
|
||||
same contract ``core/routing/models.py::ModelMetadata`` already follows.
|
||||
"""
|
||||
|
||||
provider_id: str # config key, e.g. "anthropic"
|
||||
display_name: str # human label for Settings/UI
|
||||
wire_protocol: WireProtocol # which adapter class implements it
|
||||
auth_kind: AuthKind = AuthKind.API_KEY
|
||||
default_model: str = "" # used when no model is selected
|
||||
models: Tuple[str, ...] = () # known model ids (may be empty)
|
||||
max_context: Optional[int] = None # tokens; None = unknown
|
||||
cost_per_1k_input: Optional[float] = None # USD per 1K input tokens
|
||||
cost_per_1k_output: Optional[float] = None # USD per 1K output tokens
|
||||
supports_vision: bool = False
|
||||
supports_tools: bool = True
|
||||
supports_streaming: bool = True
|
||||
requires_base_url: bool = False # gateway endpoints must be configured
|
||||
# Extra ids that should resolve to this descriptor (renames/aliases kept for
|
||||
# backwards compatibility with configs written by older app versions).
|
||||
aliases: Tuple[str, ...] = ()
|
||||
# Free-form extension point so a team can attach provider-specific hints
|
||||
# without another schema migration.
|
||||
extras: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
def __post_init__(self) -> None:
|
||||
"""Reject descriptors that could never be looked up.
|
||||
|
||||
Raising here (rather than at registration time) means a malformed
|
||||
descriptor cannot exist at all, so every consumer downstream may assume
|
||||
``provider_id`` is a usable dict key.
|
||||
"""
|
||||
if not self.provider_id:
|
||||
raise ValueError("ProviderDescriptor.provider_id must not be empty")
|
||||
if not isinstance(self.wire_protocol, WireProtocol):
|
||||
raise TypeError("ProviderDescriptor.wire_protocol must be a WireProtocol")
|
||||
|
||||
# -- identity ------------------------------------------------------- #
|
||||
@property
|
||||
def identifiers(self) -> Tuple[str, ...]:
|
||||
"""Every id this descriptor answers to (canonical id first)."""
|
||||
return (self.provider_id, *self.aliases)
|
||||
|
||||
def matches(self, provider_id: str) -> bool:
|
||||
"""Case-insensitive id/alias match — config files and CLI flags are
|
||||
typed by humans, so lookup must not be case sensitive."""
|
||||
needle = (provider_id or "").strip().lower()
|
||||
return any(needle == known.lower() for known in self.identifiers)
|
||||
|
||||
# -- capability queries --------------------------------------------- #
|
||||
def knows_model(self, model_id: str) -> bool:
|
||||
"""Whether ``model_id`` is in this provider's declared catalog.
|
||||
|
||||
A miss is NOT proof the model is unusable: gateways expose models we
|
||||
cannot enumerate offline, so callers treat this as a hint (used to
|
||||
resolve a bare model id back to its provider) and never as a gate that
|
||||
blocks a request.
|
||||
"""
|
||||
needle = (model_id or "").strip().lower()
|
||||
return any(needle == known.strip().lower() for known in self.models)
|
||||
|
||||
def has_capability(self, capability: str) -> bool:
|
||||
"""Capability check by name, mirroring the vocabulary the routing
|
||||
selector already filters on (``"vision"``, ``"tools"``, ``"streaming"``)
|
||||
so a descriptor can be fed straight into ``rank_models``."""
|
||||
return capability in self.capabilities
|
||||
|
||||
@property
|
||||
def capabilities(self) -> frozenset:
|
||||
"""Capability set in the same vocabulary as
|
||||
``core/routing/models.py::ModelMetadata.capabilities``."""
|
||||
caps = set()
|
||||
if self.supports_vision:
|
||||
caps.add("vision")
|
||||
if self.supports_tools:
|
||||
caps.add("tools")
|
||||
if self.supports_streaming:
|
||||
caps.add("streaming")
|
||||
return frozenset(caps)
|
||||
|
||||
@property
|
||||
def avg_cost_per_1k(self) -> Optional[float]:
|
||||
"""Blended input/output price, or ``None`` when either side is unknown.
|
||||
|
||||
Uses the same 1:3 input:output weighting as
|
||||
``ModelMetadata.avg_cost_per_1k`` so a descriptor and an assessment
|
||||
never disagree about what a model costs.
|
||||
"""
|
||||
ci, co = self.cost_per_1k_input, self.cost_per_1k_output
|
||||
if ci is None or co is None:
|
||||
return None
|
||||
return (ci + 3.0 * co) / 4.0
|
||||
|
||||
def resolve_model(self, requested: str = "") -> str:
|
||||
"""The model id to actually call: the caller's choice when they made
|
||||
one, otherwise this provider's default. Centralised here because every
|
||||
surface (chat, Co4E, AI-Edit) previously re-implemented the same
|
||||
``model or config_default`` fallback inline."""
|
||||
return (requested or "").strip() or self.default_model
|
||||
|
||||
# -- derivation / serialization ------------------------------------- #
|
||||
def with_models(self, models, *, default_model: str = "") -> "ProviderDescriptor":
|
||||
"""A copy carrying a freshly discovered model list.
|
||||
|
||||
Providers can enumerate their models at runtime (``list_models()``);
|
||||
because the descriptor is frozen, discovery produces a NEW descriptor
|
||||
that the registry swaps in atomically instead of mutating one that other
|
||||
threads may be reading.
|
||||
"""
|
||||
ordered = tuple(dict.fromkeys(m for m in models if m)) # de-dup, keep order
|
||||
chosen = default_model or self.default_model
|
||||
# Keep the default pointing at something real: fall back to the first
|
||||
# discovered model when the configured default vanished from the catalog.
|
||||
if ordered and chosen not in ordered:
|
||||
chosen = ordered[0]
|
||||
return replace(self, models=ordered, default_model=chosen)
|
||||
|
||||
def to_dict(self) -> Dict[str, Any]:
|
||||
"""JSON-friendly view for config persistence and the Settings UI."""
|
||||
return {
|
||||
"provider_id": self.provider_id,
|
||||
"display_name": self.display_name,
|
||||
"wire_protocol": self.wire_protocol.value,
|
||||
"auth_kind": self.auth_kind.value,
|
||||
"default_model": self.default_model,
|
||||
"models": list(self.models),
|
||||
"max_context": self.max_context,
|
||||
"cost_per_1k_input": self.cost_per_1k_input,
|
||||
"cost_per_1k_output": self.cost_per_1k_output,
|
||||
"capabilities": sorted(self.capabilities),
|
||||
"requires_base_url": self.requires_base_url,
|
||||
"aliases": list(self.aliases),
|
||||
}
|
||||
|
||||
|
||||
__all__ = ["AuthKind", "WireProtocol", "ProviderDescriptor"]
|
||||
@@ -0,0 +1 @@
|
||||
"""Domain security package: security policies, alert events, and permission types."""
|
||||
@@ -0,0 +1,110 @@
|
||||
"""Cổng chính sách cho lời gọi tool — hình dạng dữ liệu, chưa phải cài đặt.
|
||||
|
||||
BẢN ĐỀ XUẤT, chờ Team Hoa xác nhận
|
||||
==================================
|
||||
Sơ đồ phân hệ trong ``plan.md`` giao ``domain/security/`` cho Team Gamma và
|
||||
``application/conversations/tool_policy_gateway.py`` cho Team Hoa. Nên Gamma
|
||||
định nghĩa *hình dạng*, Hoa *cài đặt*.
|
||||
|
||||
Viết trước vì N3 (Co4E) cần gọi tool và Team Hoa chưa bắt đầu. Không có nó thì
|
||||
N3 phải tự phỏng đoán rồi sửa lại sau — mà phỏng đoán của một người thì tệ hơn
|
||||
một đề xuất viết ra để cả hai bên soi.
|
||||
|
||||
Nếu Hoa thấy khác, sửa file này chứ đừng đẻ kiểu thứ hai. Đổi sớm rẻ hơn đổi
|
||||
muộn: hiện chỉ N3 dùng.
|
||||
|
||||
Mô hình bám theo code đang chạy, không bịa:
|
||||
* ``core/agent_security.py::SecurityVerdict`` — allowed / reason / layer
|
||||
* ``ui/permission_dialog.py`` — hộp thoại hỏi người dùng khi
|
||||
``ctx.project_confirm_commands()`` bật (``ui/chat_panel.py:1312``)
|
||||
|
||||
Điểm khác biệt duy nhất so với hôm nay: gộp hai thứ đó thành **một câu trả lời
|
||||
ba trạng thái**, thay vì code gọi phải tự nhớ hỏi cả hai nơi.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from enum import Enum
|
||||
from typing import Any, Dict, Protocol, runtime_checkable
|
||||
|
||||
|
||||
class PolicyOutcome(str, Enum):
|
||||
"""Ba trạng thái. ``ASK`` là thứ hệ thống hiện tại đã có (hộp thoại xin
|
||||
phép) nhưng chưa được coi là một kết quả chính thức."""
|
||||
|
||||
ALLOW = "allow"
|
||||
DENY = "deny"
|
||||
ASK = "ask"
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ToolCallRequest:
|
||||
"""Một lời gọi tool đang chờ được duyệt.
|
||||
|
||||
``surface`` cho biết chỗ phát sinh — ``"cowork"``, ``"code"``, ``"co4e"``,
|
||||
``"task"``. Chính sách khác nhau theo màn: Co4E chạy nền nên không thể bật
|
||||
hộp thoại hỏi giữa chừng như Cowork.
|
||||
"""
|
||||
|
||||
name: str
|
||||
arguments: Dict[str, Any] = field(default_factory=dict)
|
||||
surface: str = "cowork"
|
||||
project_id: str = ""
|
||||
#: True nếu tool đến từ MCP server ngoài, False nếu là tool dựng sẵn.
|
||||
external: bool = False
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PolicyDecision:
|
||||
"""Câu trả lời của cổng.
|
||||
|
||||
``reason`` bắt buộc có khi DENY hoặc ASK — người dùng phải biết vì sao bị
|
||||
chặn, và ``core/audit_log.py`` cần nó để ghi lại.
|
||||
|
||||
``layer`` giữ đúng từ vựng của ``SecurityVerdict``: ``"prompt"`` |
|
||||
``"attachment"`` | ``"command"``, cộng thêm ``"policy"`` cho quyết định của
|
||||
chính cổng này.
|
||||
"""
|
||||
|
||||
outcome: PolicyOutcome
|
||||
reason: str = ""
|
||||
layer: str = "policy"
|
||||
|
||||
@property
|
||||
def allowed(self) -> bool:
|
||||
"""Tương thích với chỗ đang đọc ``SecurityVerdict.allowed``.
|
||||
|
||||
Chú ý: ``ASK`` KHÔNG phải allowed — còn phải hỏi người dùng đã.
|
||||
"""
|
||||
return self.outcome is PolicyOutcome.ALLOW
|
||||
|
||||
def __post_init__(self):
|
||||
if self.outcome is not PolicyOutcome.ALLOW and not self.reason:
|
||||
raise ValueError("DENY và ASK bắt buộc có reason — người dùng và "
|
||||
"audit log đều cần biết vì sao")
|
||||
|
||||
|
||||
def allow() -> PolicyDecision:
|
||||
return PolicyDecision(PolicyOutcome.ALLOW)
|
||||
|
||||
|
||||
def deny(reason: str, layer: str = "policy") -> PolicyDecision:
|
||||
return PolicyDecision(PolicyOutcome.DENY, reason, layer)
|
||||
|
||||
|
||||
def ask(reason: str, layer: str = "policy") -> PolicyDecision:
|
||||
return PolicyDecision(PolicyOutcome.ASK, reason, layer)
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ToolPolicyGateway(Protocol):
|
||||
"""Hỏi trước khi chạy tool. Cài đặt thật: Team Hoa (R07, hạn 29/08)."""
|
||||
|
||||
def check(self, request: ToolCallRequest) -> PolicyDecision:
|
||||
"""Được chạy tool này không.
|
||||
|
||||
KHÔNG được tự bật hộp thoại bên trong — cổng chỉ *trả lời*, còn hỏi ai
|
||||
và hỏi thế nào là việc của tầng giao diện. Có vậy thì Co4E chạy nền mới
|
||||
dùng chung cổng được với Cowork chạy tương tác.
|
||||
"""
|
||||
...
|
||||
@@ -0,0 +1 @@
|
||||
"""Domain tasks package: task definitions and deterministic schedule calculators."""
|
||||
@@ -0,0 +1 @@
|
||||
"""Domain tools package: tool descriptors, capability scopes, and registry interfaces."""
|
||||
@@ -0,0 +1,114 @@
|
||||
"""Bản ghi "một lần chạy flow" — DTO thuần Python cho tầng domain.
|
||||
|
||||
Bối cảnh: ``Co4ERunManager``/``RunHandle`` cũ (``core/co4e_run_manager.py``)
|
||||
trộn ba việc vào một ``QObject``: (1) dữ liệu một run cần nhớ để hiện Flow
|
||||
Status, (2) logic chạy job trên ``AgentWorker``/``QThread``, và (3) logic
|
||||
đọc/ghi lịch sử ra đĩa. Tách phần (1) ra thành ``RunRecord`` ở đây giúp nó độc
|
||||
lập với Qt và với việc đọc/ghi đĩa — đúng quy ước ``domain/__init__.py``: domain
|
||||
không được biết PySide6 tồn tại và không được chạm đĩa/mạng. Phần (2) và (3)
|
||||
chuyển sang ``application/workflows/co4e_workflow_service.py``
|
||||
(``Co4EWorkflowService``), nơi được phép import ``core/`` và làm việc với đĩa.
|
||||
|
||||
Vì sao trường ``wf`` là dict thô chứ không phải đối tượng ``Workflow``: lớp
|
||||
``Workflow`` sống ở ``core/co4e.py``, và việc dựng nó từ/thành dict
|
||||
(``workflow_to_dict``/``workflow_from_dict``) nằm trong module đó. Domain
|
||||
không được import ``cowork_local.core.*``, nên ``RunRecord`` giữ nguyên đúng
|
||||
hình dạng dữ liệu mà bản ghi lịch sử đã có sẵn trên đĩa hôm nay: một dict thô
|
||||
(kết quả ``workflow_to_dict``) hoặc ``None``. Việc quy đổi dict <-> đối tượng
|
||||
``Workflow`` là việc của tầng application, nơi được phép import ``core``.
|
||||
|
||||
Quirk giữ nguyên có chủ ý — đã bị "đóng đinh" bởi
|
||||
``tests/characterization/test_co4e_run_manager_behavior.py`` (quirk #1 và #7
|
||||
trong docstring đầu file đó, xem thêm ``RunHandle.to_record``/``from_record``
|
||||
gốc) — ĐỪNG "dọn" các chỗ này khi đọc code dưới đây, chúng trông như bug nhưng
|
||||
là hành vi đã được test khẳng định:
|
||||
* ``total`` âm bị ``max(0, total)`` kẹp về 0 ngay lúc khởi tạo, không giữ
|
||||
nguyên giá trị âm.
|
||||
* ``from_dict()`` đổi ``status == "running"`` đọc từ đĩa thành ``"stopped"``
|
||||
(lý do: app tắt giữa lúc một run đang "running" thì worker của nó đã mất
|
||||
theo, nên đọc lại không còn coi là đang chạy) — nhưng ``to_dict()`` vẫn ghi
|
||||
đúng ``"running"`` xuống đĩa tại thời điểm lưu. Đây là một round-trip
|
||||
*không đối xứng* có chủ ý.
|
||||
* ``from_dict({})``/``from_dict(None)`` mặc định ``status`` là ``"done"``
|
||||
(không phải ``"running"``) — nên KHÔNG bị nhánh phía trên đổi thành
|
||||
"stopped".
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict, Optional
|
||||
|
||||
|
||||
class RunRecord:
|
||||
"""DTO domain: trạng thái sống của một lần chạy flow, thuần dữ liệu.
|
||||
|
||||
Vai trò: đây là "danh từ" mà ``Co4EWorkflowService`` (application/) đọc/ghi
|
||||
và mà UI Flow Status hiển thị — không có hành vi chạy worker, không đọc/ghi
|
||||
đĩa. Nó ở tầng domain vì đây là quy tắc nghiệp vụ ổn định (hình dạng một
|
||||
lần chạy flow cần nhớ những gì) độc lập với Qt lẫn với cơ chế lưu trữ.
|
||||
"""
|
||||
|
||||
def __init__(self, run_id: str, wf_id: str, name: str, total: int,
|
||||
plan_mode: bool, manual: bool, created_by: str = "", created_at: str = "",
|
||||
project_id: str = ""):
|
||||
self.id = run_id
|
||||
self.wf_id = wf_id
|
||||
self.name = name
|
||||
self.project_id = project_id # workspace run này thuộc về (Flow Status lọc theo project)
|
||||
self.total = max(0, total) # quirk cố ý: total âm bị kẹp về 0, xem docstring đầu file
|
||||
self.done = 0
|
||||
self.status = "running" # running | done | error | stopped
|
||||
self.plan_mode = plan_mode
|
||||
self.manual = manual
|
||||
self.created_by = created_by
|
||||
self.created_at = created_at
|
||||
self.error = ""
|
||||
self.node_status: Dict[str, str] = {}
|
||||
self.wf: Optional[dict] = None # snapshot workflow dạng dict thô (xem docstring đầu file)
|
||||
self.out_dir = "" # thư mục workspace mà run này ghi file vào
|
||||
|
||||
@property
|
||||
def running(self) -> bool:
|
||||
return self.status == "running"
|
||||
|
||||
def progress_text(self) -> str:
|
||||
return f"{self.done}/{self.total}" if self.total else self.status
|
||||
|
||||
# ---- (de)serialization --------------------------------------------
|
||||
def to_dict(self) -> dict:
|
||||
"""Hình dạng bản ghi lịch sử trên đĩa.
|
||||
|
||||
PHẢI khớp đúng bộ khoá mà ``RunHandle.to_record()`` gốc
|
||||
(``core/co4e_run_manager.py``) đang ghi hôm nay — file JSON lịch sử cũ
|
||||
và mới dùng chung một định dạng trong lúc cả hai lớp còn chạy song
|
||||
song (bản cũ chưa bị xoá).
|
||||
"""
|
||||
return {
|
||||
"id": self.id, "wf_id": self.wf_id, "name": self.name,
|
||||
"total": self.total, "done": self.done, "status": self.status,
|
||||
"plan_mode": self.plan_mode, "manual": self.manual,
|
||||
"created_by": self.created_by, "created_at": self.created_at,
|
||||
"error": self.error, "node_status": dict(self.node_status),
|
||||
"wf": self.wf, "out_dir": self.out_dir, "project_id": self.project_id,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, rec: dict) -> "RunRecord":
|
||||
rec = dict(rec or {})
|
||||
r = cls(str(rec.get("id", "")), str(rec.get("wf_id", "")),
|
||||
rec.get("name", ""), int(rec.get("total", 0) or 0),
|
||||
bool(rec.get("plan_mode")), bool(rec.get("manual")),
|
||||
created_by=rec.get("created_by", ""), created_at=rec.get("created_at", ""))
|
||||
r.done = int(rec.get("done", 0) or 0)
|
||||
r.status = rec.get("status", "done")
|
||||
# quirk cố ý (xem docstring đầu file): round-trip không đối xứng —
|
||||
# "running" đọc lại từ đĩa luôn bị chốt thành "stopped".
|
||||
if r.status == "running":
|
||||
r.status = "stopped"
|
||||
r.error = rec.get("error", "")
|
||||
r.node_status = dict(rec.get("node_status") or {})
|
||||
r.out_dir = rec.get("out_dir", "")
|
||||
r.project_id = rec.get("project_id", "")
|
||||
# Giữ nguyên dict thô -- KHONG parse thanh doi tuong Workflow o day (do
|
||||
# la viec cua tang application, xem docstring dau file).
|
||||
r.wf = rec.get("wf")
|
||||
return r
|
||||
@@ -0,0 +1 @@
|
||||
"""Domain workspaces package: immutable WorkspaceSession definitions."""
|
||||
@@ -0,0 +1,344 @@
|
||||
"""Chuỗi hiển thị — phần agents_admin_tab.
|
||||
|
||||
Cắt ra từ ``i18n.py``: một dict 3.000 dòng không mở nổi để sửa một chữ.
|
||||
Cắt theo mốc phân đoạn có sẵn, nên mỗi cụm vẫn là các màn đi liền nhau.
|
||||
``i18n.py`` gộp tất cả lại thành ``STRINGS``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict
|
||||
|
||||
STRINGS: Dict[str, Dict[str, str]] = {
|
||||
"co4e.tt_flow_name": {"en": "Flow name", "ja": "フロー名", "vi": "Tên flow"},
|
||||
"co4e.tt_add_step": {"en": "Add a step to the canvas", "ja": "キャンバスにステップを追加",
|
||||
"vi": "Thêm một bước vào canvas"},
|
||||
"co4e.tt_zoom_in": {"en": "Zoom in (Ctrl+wheel / Ctrl++)", "ja": "拡大(Ctrl+ホイール / Ctrl++)",
|
||||
"vi": "Phóng to (Ctrl+lăn chuột / Ctrl++)"},
|
||||
"co4e.tt_zoom_out": {"en": "Zoom out (Ctrl+wheel / Ctrl+-)", "ja": "縮小(Ctrl+ホイール / Ctrl+-)",
|
||||
"vi": "Thu nhỏ (Ctrl+lăn chuột / Ctrl+-)"},
|
||||
"co4e.run_bg": {"en": "Run", "ja": "実行", "vi": "Chạy"},
|
||||
"co4e.tt_run_bg": {
|
||||
"en": "Run the selected flow in the background — several flows run in parallel",
|
||||
"ja": "選択フローをバックグラウンド実行 — 複数フローを並列実行",
|
||||
"vi": "Chạy flow đã chọn ở nền — nhiều flow chạy song song"},
|
||||
"co4e.running_flows": {"en": "Running flows", "ja": "実行中のフロー", "vi": "Flow đang chạy"},
|
||||
"co4e.runs_tab": {"en": "Flow Status", "ja": "フロー状態", "vi": "Flow Status"},
|
||||
# The flow tab strip was removed, so its pinned Flow Status tab became a
|
||||
# toggle in the flow toolbar — and that page needs its own way back.
|
||||
"co4e.tt_runs_tab": {
|
||||
"en": "Show every flow run", "ja": "すべてのフロー実行を表示",
|
||||
"vi": "Xem toàn bộ lần chạy flow"},
|
||||
"co4e.back_to_flow": {"en": "Back to flow", "ja": "フローに戻る", "vi": "Về flow"},
|
||||
"co4e.new_flow_ready": {
|
||||
"en": "New flow — type a name, then drag agents onto the canvas",
|
||||
"ja": "新しいフロー — 名前を入力し、エージェントをキャンバスへ",
|
||||
"vi": "Flow mới — đặt tên rồi kéo agent vào canvas"},
|
||||
"co4e.tt_back_to_flow": {
|
||||
"en": "Back to the flow editor", "ja": "フローエディタに戻る",
|
||||
"vi": "Quay lại màn dựng flow"},
|
||||
"co4e.runs_tab_n": {"en": "Flow Status ({n})", "ja": "フロー状態 ({n})", "vi": "Flow Status ({n})"},
|
||||
"co4e.runs_col_flow": {"en": "Flow", "ja": "フロー", "vi": "Flow"},
|
||||
"co4e.runs_col_status": {"en": "Status", "ja": "状態", "vi": "Trạng thái"},
|
||||
"co4e.runs_col_steps": {"en": "Steps", "ja": "ステップ", "vi": "Bước"},
|
||||
"co4e.runs_col_by": {"en": "Created by", "ja": "作成者", "vi": "Người tạo"},
|
||||
"co4e.runs_col_at": {"en": "Created at", "ja": "作成日時", "vi": "Ngày tạo"},
|
||||
"co4e.tt_runs_list": {
|
||||
"en": "Live status of every running/finished flow — always up to date. Double-click a run to run that flow again.",
|
||||
"ja": "実行中/完了フローのライブ状態 — 常に最新。実行をダブルクリックでそのフローを再実行。",
|
||||
"vi": "Trạng thái trực tiếp của mọi flow đang chạy/đã xong — luôn mới nhất. Nhấp đúp để chạy lại flow đó."},
|
||||
"co4e.flow_gone": {"en": "That flow no longer exists.", "ja": "そのフローは存在しません。",
|
||||
"vi": "Flow đó không còn tồn tại."},
|
||||
"co4e.rename": {"en": "Rename", "ja": "名前を変更", "vi": "Đổi tên"},
|
||||
"co4e.rename_prompt": {"en": "New flow name (name it by its function / task):",
|
||||
"ja": "新しいフロー名(機能/タスクで命名):",
|
||||
"vi": "Tên flow mới (đặt theo chức năng / task):"},
|
||||
"co4e.renamed_msg": {"en": "Renamed to: {name}", "ja": "名前変更: {name}", "vi": "Đã đổi tên: {name}"},
|
||||
"co4e.duplicate": {"en": "Duplicate", "ja": "複製", "vi": "Nhân bản"},
|
||||
"co4e.viewing_flow": {"en": "Viewing flow: {name}", "ja": "フロー表示: {name}", "vi": "Đang xem flow: {name}"},
|
||||
"co4e.stop": {"en": "Stop", "ja": "停止", "vi": "Dừng"},
|
||||
"co4e.tt_stop_run": {"en": "Stop the selected run (or all runs if none selected)",
|
||||
"ja": "選択した実行を停止(未選択なら全実行)", "vi": "Dừng lần chạy đã chọn (hoặc tất cả nếu chưa chọn)"},
|
||||
"co4e.clear_done": {"en": "Clear done", "ja": "完了を消去", "vi": "Xóa đã xong"},
|
||||
"co4e.tt_clear_runs": {"en": "Remove finished/stopped runs from the list",
|
||||
"ja": "完了/停止した実行を一覧から削除", "vi": "Bỏ các lần chạy đã xong/đã dừng khỏi danh sách"},
|
||||
"co4e.select_flow": {"en": "Select a flow first.", "ja": "先にフローを選択してください。",
|
||||
"vi": "Hãy chọn một flow trước."},
|
||||
"co4e.delete_run": {"en": "Delete", "ja": "削除", "vi": "Xóa"},
|
||||
"co4e.tt_delete_run": {"en": "Delete the selected run from the history",
|
||||
"ja": "選択した実行を履歴から削除", "vi": "Xóa lần chạy đang chọn khỏi lịch sử"},
|
||||
"co4e.open_run": {"en": "Open flow", "ja": "フローを開く", "vi": "Mở flow"},
|
||||
"co4e.open_output": {"en": "Open output folder", "ja": "出力フォルダを開く", "vi": "Mở thư mục output"},
|
||||
"co4e.open_output_link": {
|
||||
"en": "📂 Open output folder", "ja": "📂 出力フォルダを開く", "vi": "📂 Mở thư mục output"},
|
||||
"co4e.tt_open_workspace": {
|
||||
"en": "Open the workspace folder where flow outputs are saved:\n{path}",
|
||||
"ja": "フローの出力が保存されるワークスペースフォルダを開く:\n{path}",
|
||||
"vi": "Mở thư mục workspace nơi lưu output của flow:\n{path}"},
|
||||
"co4e.select_run": {"en": "Select a run first.", "ja": "先に実行を選択してください。",
|
||||
"vi": "Hãy chọn một lần chạy trước."},
|
||||
"co4e.rename_run": {"en": "Rename", "ja": "名前変更", "vi": "Đổi tên"},
|
||||
"co4e.tt_rename_run": {"en": "Rename the selected flow run",
|
||||
"ja": "選択した実行の名前を変更", "vi": "Đổi tên lần chạy đang chọn"},
|
||||
"co4e.rename_run_label": {"en": "New flow name:", "ja": "新しいフロー名:", "vi": "Tên flow mới:"},
|
||||
"co4e.run_done_title": {"en": "Flow finished", "ja": "フロー完了", "vi": "Flow đã xong"},
|
||||
"co4e.run_done_popup": {
|
||||
"en": "Flow \"{name}\" finished — {status}.",
|
||||
"ja": "フロー「{name}」が完了しました — {status}。",
|
||||
"vi": "Flow \"{name}\" đã chạy xong — {status}."},
|
||||
"co4e.duplicated_msg": {"en": "Duplicated: {name}", "ja": "複製しました: {name}", "vi": "Đã nhân bản: {name}"},
|
||||
"co4e.bg_started": {"en": "▶ Started in background: {name}", "ja": "▶ バックグラウンドで開始: {name}",
|
||||
"vi": "▶ Đã chạy nền: {name}"},
|
||||
"co4e.bg_done": {"en": "Flow '{name}': {status}", "ja": "フロー '{name}': {status}",
|
||||
"vi": "Flow '{name}': {status}"},
|
||||
"co4e.tool_failed": {"en": "⚠ tool failed: {name}", "ja": "⚠ ツール失敗: {name}", "vi": "⚠ tool lỗi: {name}"},
|
||||
"co4e.manual_started": {"en": "▶ Manual run: {name} — advance with Run/Next step.",
|
||||
"ja": "▶ 手動実行: {name} — 「実行/次へ」で進む。",
|
||||
"vi": "▶ Chạy thủ công: {name} — bấm Chạy/Bước tiếp để tiến."},
|
||||
"co4e.manual_step": {"en": "▶ Step {i}/{n}: {label}", "ja": "▶ ステップ {i}/{n}: {label}",
|
||||
"vi": "▶ Bước {i}/{n}: {label}"},
|
||||
"co4e.status.running": {"en": "running", "ja": "実行中", "vi": "đang chạy"},
|
||||
"co4e.status.done": {"en": "done", "ja": "完了", "vi": "xong"},
|
||||
"co4e.status.error": {"en": "error", "ja": "エラー", "vi": "lỗi"},
|
||||
"co4e.status.stopped": {"en": "stopped", "ja": "停止", "vi": "đã dừng"},
|
||||
"co4e.chat_placeholder": {
|
||||
"en": "Chat with the flow — use /agent:<name> or /skill:<name>",
|
||||
"ja": "フローとチャット — /agent:<name> または /skill:<name>",
|
||||
"vi": "Chat với flow — dùng /agent:<name> hoặc /skill:<name>"},
|
||||
"co4e.send": {"en": "Send", "ja": "送信", "vi": "Gửi"},
|
||||
"co4e.tab_basic": {"en": "Basic", "ja": "基本", "vi": "Cơ bản"},
|
||||
"co4e.tab_model_perm": {"en": "Model & Permission", "ja": "モデルと権限", "vi": "Model & Quyền"},
|
||||
"co4e.tab_skills_files": {"en": "Skills & Files", "ja": "スキルとファイル", "vi": "Skills & Tệp"},
|
||||
"co4e.f_label": {"en": "Label", "ja": "ラベル", "vi": "Nhãn"},
|
||||
"co4e.f_role": {"en": "Role", "ja": "ロール", "vi": "Vai trò"},
|
||||
"co4e.f_name": {"en": "Name", "ja": "名前", "vi": "Tên"},
|
||||
"co4e.f_icon": {"en": "Icon", "ja": "アイコン", "vi": "Icon"},
|
||||
"co4e.f_icon_placeholder": {"en": "icon name (optional)", "ja": "アイコン名(任意)", "vi": "tên icon (tùy chọn)"},
|
||||
"co4e.f_instructions": {"en": "Instructions", "ja": "指示", "vi": "Hướng dẫn"},
|
||||
"co4e.f_context": {"en": "Context", "ja": "コンテキスト", "vi": "Ngữ cảnh"},
|
||||
"co4e.f_context_placeholder": {
|
||||
"en": "Extra background/info for this agent or step (added to its prompt at run time).",
|
||||
"ja": "このエージェント/ステップ用の追加情報(実行時にプロンプトへ追加されます)。",
|
||||
"vi": "Thông tin/ngữ cảnh bổ sung cho agent hoặc step này (thêm vào prompt khi chạy)."},
|
||||
"co4e.f_model": {"en": "Model", "ja": "モデル", "vi": "Model"},
|
||||
"co4e.f_permission": {"en": "Permissions", "ja": "権限", "vi": "Quyền"},
|
||||
"co4e.f_self_verify": {"en": "Self-verify", "ja": "自己検証", "vi": "Tự kiểm tra"},
|
||||
"co4e.f_verify_rounds": {"en": "rounds", "ja": "回数", "vi": "vòng"},
|
||||
"co4e.f_skills": {"en": "Skills", "ja": "スキル", "vi": "Skills"},
|
||||
"co4e.f_attachments": {"en": "Attachments", "ja": "添付ファイル", "vi": "Tệp đính kèm"},
|
||||
"co4e.attach_add": {"en": "Attach files", "ja": "ファイル添付", "vi": "Đính kèm tệp"},
|
||||
"co4e.attach_remove": {"en": "Remove", "ja": "削除", "vi": "Bỏ"},
|
||||
"co4e.f_subagents": {"en": "Parallel agents", "ja": "並列エージェント", "vi": "Agent song song"},
|
||||
"co4e.perm.inherit": {"en": "Inherit", "ja": "継承", "vi": "Kế thừa"},
|
||||
"co4e.perm.read-only": {"en": "Read-only", "ja": "読み取り専用", "vi": "Chỉ đọc"},
|
||||
"co4e.perm.standard": {"en": "Standard", "ja": "標準", "vi": "Tiêu chuẩn"},
|
||||
"co4e.perm.full": {"en": "Full", "ja": "フル", "vi": "Toàn quyền"},
|
||||
"co4e.add_subagent": {"en": "Add", "ja": "追加", "vi": "Thêm"},
|
||||
"co4e.del_subagent": {"en": "Remove", "ja": "削除", "vi": "Bỏ"},
|
||||
"co4e.run_this_step": {"en": "Run this step", "ja": "このステップを実行", "vi": "Chạy bước này"},
|
||||
"co4e.run_from_here": {"en": "Run from here", "ja": "ここから実行", "vi": "Chạy từ đây"},
|
||||
"co4e.delete_step": {"en": "Delete step", "ja": "ステップ削除", "vi": "Xóa bước"},
|
||||
"co4e.load_models_tooltip": {
|
||||
"en": "Load available models", "ja": "利用可能なモデルを取得", "vi": "Tải danh sách model"},
|
||||
"co4e.agent_edit_title": {"en": "Edit agent", "ja": "エージェント編集", "vi": "Sửa agent"},
|
||||
"co4e.agent_new_title": {"en": "New agent", "ja": "新規エージェント", "vi": "Agent mới"},
|
||||
"co4e.saved_msg": {"en": "Saved flow: {name}", "ja": "フローを保存: {name}", "vi": "Đã lưu flow: {name}"},
|
||||
"co4e.select_custom_agent": {
|
||||
"en": "Select a custom agent first.", "ja": "先にカスタムエージェントを選択してください。",
|
||||
"vi": "Hãy chọn một agent tùy chỉnh trước."},
|
||||
"co4e.no_steps": {"en": "Add at least one step first.", "ja": "先にステップを追加してください。",
|
||||
"vi": "Hãy thêm ít nhất một bước."},
|
||||
"co4e.run_started": {"en": "▶ Running flow: {name}", "ja": "▶ フロー実行中: {name}",
|
||||
"vi": "▶ Đang chạy flow: {name}"},
|
||||
"co4e.run_done": {"en": "✓ Flow finished.", "ja": "✓ フロー完了。", "vi": "✓ Flow xong."},
|
||||
"co4e.run_execute_phase": {
|
||||
"en": "▶ Plan done — now executing…", "ja": "▶ 計画完了 — 実行中…",
|
||||
"vi": "▶ Xong plan — đang thực thi…"},
|
||||
"co4e.agent_not_found": {
|
||||
"en": "Agent '{name}' not found.", "ja": "エージェント '{name}' が見つかりません。",
|
||||
"vi": "Không tìm thấy agent '{name}'."},
|
||||
|
||||
# ---- agents_admin_tab.py — Admin-only agent catalog -------------------
|
||||
"agents_admin.page_title": {"en": "Agents Admin", "ja": "Agents Admin", "vi": "Agents Admin"},
|
||||
"agents_admin.edit_row_tooltip": {"en": "Edit", "ja": "編集", "vi": "Sửa"},
|
||||
"agents_admin.delete_row_tooltip": {"en": "Delete", "ja": "削除", "vi": "Xóa"},
|
||||
"agents_admin.hint": {
|
||||
"en": "System-management agents shared across every machine (stored in the shared accounts folder): the help agent and Schedule Task executors. These are NOT the agents you pick in Cowork or Co4E.",
|
||||
"ja": "全マシンで共有されるシステム管理用エージェント(共有フォルダーに保存):ヘルプエージェントやスケジュールタスクの実行エージェントなど。CoworkやCo4Eで選択するエージェントではありません。",
|
||||
"vi": "Agent quản lý hệ thống, dùng chung mọi máy (lưu trong thư mục dùng chung): agent trợ giúp và agent chạy Schedule Task. Đây KHÔNG phải agent để chọn trong Cowork hay Co4E."},
|
||||
"agents_admin.add_title": {"en": "Add agent", "ja": "エージェント追加", "vi": "Thêm agent"},
|
||||
"agents_admin.edit_title": {"en": "Edit agent", "ja": "エージェント編集", "vi": "Sửa agent"},
|
||||
"agents_admin.delete_title": {"en": "Delete agent", "ja": "エージェント削除", "vi": "Xóa agent"},
|
||||
"agents_admin.delete_confirm": {
|
||||
"en": "Delete agent \"{name}\"?", "ja": "エージェント「{name}」を削除しますか?",
|
||||
"vi": "Xóa agent \"{name}\"?"},
|
||||
"agents_admin.add_btn": {"en": "Add", "ja": "追加", "vi": "Thêm"},
|
||||
"agents_admin.f_name": {"en": "Name", "ja": "名前", "vi": "Tên"},
|
||||
"agents_admin.f_kind": {"en": "App function", "ja": "アプリ機能", "vi": "Chức năng App"},
|
||||
"agents_admin.f_prompt": {"en": "Instructions", "ja": "指示", "vi": "Chỉ dẫn"},
|
||||
"agents_admin.f_prompt_placeholder": {
|
||||
"en": "Extra instructions this agent always follows (optional)…",
|
||||
"ja": "このエージェントが常に従う追加指示(任意)…",
|
||||
"vi": "Chỉ dẫn bổ sung agent này luôn tuân theo (tùy chọn)…"},
|
||||
"agents_admin.f_provider": {"en": "Provider", "ja": "プロバイダー", "vi": "Provider"},
|
||||
"agents_admin.provider_default": {
|
||||
"en": "(machine's active provider)", "ja": "(各マシンの現在のプロバイダー)",
|
||||
"vi": "(provider hiện tại của máy)"},
|
||||
"agents_admin.f_model": {"en": "Model", "ja": "モデル", "vi": "Model"},
|
||||
"agents_admin.f_model_placeholder": {
|
||||
"en": "empty = each machine's Settings model (currently: {model})",
|
||||
"ja": "空欄 = 各マシンの設定モデル(現在: {model})",
|
||||
"vi": "để trống = model trong Settings của từng máy (hiện tại: {model})"},
|
||||
"agents_admin.load_models_tooltip": {
|
||||
"en": "Fetch this provider's real model list so you can pick a specific one from the dropdown.",
|
||||
"ja": "このプロバイダーの実際のモデル一覧を取得し、ドロップダウンから選択できるようにします。",
|
||||
"vi": "Lấy danh sách model thực tế của provider này để chọn từ dropdown."},
|
||||
"agents_admin.load_models_empty": {
|
||||
"en": "No models were returned — check the provider's settings/connection.",
|
||||
"ja": "モデルが取得できませんでした。プロバイダーの設定/接続を確認してください。",
|
||||
"vi": "Không lấy được model nào — kiểm tra lại cấu hình/kết nối provider."},
|
||||
"agents_admin.f_enabled": {"en": "Enabled", "ja": "有効", "vi": "Kích hoạt"},
|
||||
"agents_admin.col_name": {"en": "Name", "ja": "名前", "vi": "Tên"},
|
||||
"agents_admin.col_kind": {"en": "Function", "ja": "機能", "vi": "Chức năng"},
|
||||
"agents_admin.col_model": {"en": "Model", "ja": "モデル", "vi": "Model"},
|
||||
"agents_admin.col_enabled": {"en": "Enabled", "ja": "有効", "vi": "Kích hoạt"},
|
||||
"agents_admin.col_status": {"en": "Status", "ja": "状態", "vi": "Trạng thái"},
|
||||
"agents_admin.col_updated": {"en": "Updated", "ja": "更新", "vi": "Cập nhật"},
|
||||
"agents_admin.check_btn": {"en": "Check all", "ja": "すべてチェック", "vi": "Kiểm tra tất cả"},
|
||||
"agents_admin.check_tooltip": {
|
||||
"en": "Check each agent's effective provider/model connectivity",
|
||||
"ja": "各エージェントの実効プロバイダ/モデルの接続性を確認",
|
||||
"vi": "Kiểm tra kết nối provider/model hiệu lực của từng agent"},
|
||||
"agents_admin.status_unchecked": {"en": "— (not checked)", "ja": "— (未チェック)", "vi": "— (chưa kiểm tra)"},
|
||||
"agents_admin.status_unchecked_tip": {
|
||||
"en": "Press Check to test whether this agent's provider/model is reachable",
|
||||
"ja": "「チェック」でこのエージェントのプロバイダ/モデルへの到達性をテスト",
|
||||
"vi": "Nhấn Kiểm tra để test agent này có kết nối được provider/model không"},
|
||||
"agents_admin.status_checking": {"en": "checking…", "ja": "確認中…", "vi": "đang kiểm tra…"},
|
||||
"agents_admin.status_ok": {"en": "Active", "ja": "稼働中", "vi": "Hoạt động"},
|
||||
"agents_admin.status_bad": {"en": "Error", "ja": "エラー", "vi": "Lỗi"},
|
||||
"agents_admin.default_model": {
|
||||
"en": "(Settings default: {model})", "ja": "(設定既定: {model})",
|
||||
"vi": "(mặc định Settings: {model})"},
|
||||
"agents_admin.kind.search": {"en": "Search", "ja": "検索", "vi": "Tìm kiếm"},
|
||||
"agents_admin.kind.monitor": {"en": "Monitoring", "ja": "監視", "vi": "Giám sát"},
|
||||
"agents_admin.kind.cowork": {"en": "Cowork chat", "ja": "Cowork チャット", "vi": "Cowork chat"},
|
||||
"agents_admin.kind.graphrag": {"en": "GraphRAG / Knowledge", "ja": "GraphRAG / ナレッジ", "vi": "GraphRAG / Tri thức"},
|
||||
"agents_admin.kind.schedule": {"en": "Schedule Task", "ja": "スケジュールタスク", "vi": "Schedule Task"},
|
||||
"agents_admin.kind.security": {"en": "Security", "ja": "セキュリティ", "vi": "Bảo mật"},
|
||||
"agents_admin.kind.help": {"en": "App Help", "ja": "アプリヘルプ", "vi": "Trợ giúp App"},
|
||||
|
||||
"monitoring.filter_placeholder": {
|
||||
"en": "Filter rows (or type a question and press )…",
|
||||
"ja": "行をフィルター(質問を入力しても可)…",
|
||||
"vi": "Lọc dòng (hoặc gõ câu hỏi rồi bấm )…"},
|
||||
"monitoring.ai_filter_btn": {"en": "AI", "ja": "AI", "vi": "AI"},
|
||||
"monitoring.pricing_title": {
|
||||
"en": "Model pricing (USD / 1M tokens)", "ja": "モデル価格表 (USD / 100万トークン)",
|
||||
"vi": "Bảng giá model (USD / 1 triệu token)"},
|
||||
"monitoring.pricing_col_model": {"en": "Model", "ja": "モデル", "vi": "Model"},
|
||||
"monitoring.pricing_col_in": {"en": "In", "ja": "入力", "vi": "In"},
|
||||
"monitoring.pricing_col_out": {"en": "Out", "ja": "出力", "vi": "Out"},
|
||||
"monitoring.pricing_col_cache": {"en": "Cache", "ja": "キャッシュ", "vi": "Cache"},
|
||||
"monitoring.pricing_add_btn": {"en": "Add model", "ja": "モデル追加", "vi": "Thêm model"},
|
||||
"monitoring.pricing_del_btn": {"en": "Remove", "ja": "削除", "vi": "Xóa"},
|
||||
"monitoring.pricing_link_label": {
|
||||
"en": "Reference:", "ja": "参考リンク:", "vi": "Link tham khảo:"},
|
||||
"monitoring.pricing_no_link": {
|
||||
"en": "No reference link set (Settings → Parameter → Pricing reference link).",
|
||||
"ja": "参考リンク未設定(設定 → Parameter)。",
|
||||
"vi": "Chưa đặt link tham khảo (Settings → Parameter → Link bảng giá)."},
|
||||
"monitoring.ai_filter_tooltip": {
|
||||
"en": "AI turns your question into a filter keyword (e.g. \"which commands failed today?\").",
|
||||
"ja": "質問をAIがフィルターキーワードに変換します。",
|
||||
"vi": "AI chuyển câu hỏi thành từ khóa lọc (vd: \"hôm nay lệnh nào bị lỗi?\")."},
|
||||
"monitoring.security_detail_title": {
|
||||
"en": "Event details", "ja": "イベント詳細", "vi": "Chi tiết sự kiện"},
|
||||
"monitoring.security_detail_close": {
|
||||
"en": "Close", "ja": "閉じる", "vi": "Đóng"},
|
||||
"monitoring.security_events_title": {
|
||||
"en": "Security Events", "ja": "セキュリティイベント", "vi": "Sự kiện bảo mật"},
|
||||
"monitoring.mcp_history_title": {
|
||||
"en": "MCP Call History", "ja": "MCP 呼び出し履歴", "vi": "Lịch sử gọi MCP"},
|
||||
"monitoring.action_logs_title": {
|
||||
"en": "Action Logs", "ja": "アクションログ", "vi": "Nhật ký hành động"},
|
||||
"monitoring.col_detail_block": {
|
||||
"en": "Block detail", "ja": "ブロック詳細", "vi": "Chi tiết chặn"},
|
||||
|
||||
# ---- event-detail panel (ui-audit_v2.html openDetail()) --------------
|
||||
"monitoring.detail_section_general": {
|
||||
"en": "General info", "ja": "基本情報", "vi": "Thông tin chung"},
|
||||
"monitoring.detail_section_action": {
|
||||
"en": "Action", "ja": "アクション", "vi": "Hành động"},
|
||||
"monitoring.detail_section_metadata": {
|
||||
"en": "Metadata", "ja": "メタデータ", "vi": "Metadata"},
|
||||
"monitoring.detail_type": {"en": "Type", "ja": "種類", "vi": "Loại"},
|
||||
"monitoring.detail_status": {"en": "Status", "ja": "ステータス", "vi": "Trạng thái"},
|
||||
"monitoring.detail_event_id": {"en": "Event ID", "ja": "イベントID", "vi": "Event ID"},
|
||||
"monitoring.detail_policy": {"en": "Policy", "ja": "ポリシー", "vi": "Policy"},
|
||||
"monitoring.detail_severity": {"en": "Severity", "ja": "重大度", "vi": "Severity"},
|
||||
"monitoring.detail_copy": {"en": "Copy", "ja": "コピー", "vi": "Copy"},
|
||||
"monitoring.detail_copied": {"en": "Copied", "ja": "コピー済み", "vi": "Đã copy"},
|
||||
|
||||
# Trạng thái pill — which rule fired, phrased as the enforcement outcome
|
||||
# (distinct wording from the Loại/action_* labels below, matching
|
||||
# ui-audit_v2.html's statusInfo() vs actionLabel).
|
||||
"monitoring.status_blocked": {"en": "Blocked", "ja": "ブロック済み", "vi": "Đã chặn"},
|
||||
"monitoring.status_path": {"en": "Path blocked", "ja": "パスをブロック", "vi": "Path chặn"},
|
||||
"monitoring.status_network": {"en": "Network blocked", "ja": "ネットワークをブロック", "vi": "Mạng chặn"},
|
||||
"monitoring.status_secret": {"en": "Secret leaked", "ja": "シークレット漏洩", "vi": "Bí mật lộ"},
|
||||
"monitoring.status_ok": {"en": "Succeeded", "ja": "成功", "vi": "Thành công"},
|
||||
"monitoring.status_failed": {"en": "Failed", "ja": "失敗", "vi": "Thất bại"},
|
||||
|
||||
"monitoring.severity_critical": {"en": "CRITICAL", "ja": "CRITICAL", "vi": "CRITICAL"},
|
||||
"monitoring.severity_medium": {"en": "MEDIUM", "ja": "MEDIUM", "vi": "MEDIUM"},
|
||||
"monitoring.severity_info": {"en": "INFO", "ja": "INFO", "vi": "INFO"},
|
||||
|
||||
# Loại field — a human label for the raw event name (audit_log ``name``).
|
||||
"monitoring.action_prompt": {"en": "Risky prompt", "ja": "危険なプロンプト", "vi": "Prompt rủi ro"},
|
||||
"monitoring.action_dangerous_command": {
|
||||
"en": "Dangerous command", "ja": "危険なコマンド", "vi": "Lệnh nguy hiểm"},
|
||||
"monitoring.action_install_package": {
|
||||
"en": "Package install", "ja": "パッケージインストール", "vi": "Cài đặt gói"},
|
||||
"monitoring.action_path_outside_sandbox": {
|
||||
"en": "Path outside sandbox", "ja": "サンドボックス外のパス", "vi": "Path ngoài sandbox"},
|
||||
"monitoring.action_network_blocked": {
|
||||
"en": "Network blocked", "ja": "ネットワークブロック", "vi": "Mạng bị chặn"},
|
||||
"monitoring.action_secret_in_output": {
|
||||
"en": "Secret disclosed", "ja": "シークレット漏洩", "vi": "Tiết lộ bí mật"},
|
||||
|
||||
"monitoring.overview_activity_title": {
|
||||
"en": "Recent log", "ja": "最近のログ", "vi": "Nhật ký gần đây"},
|
||||
"monitoring.overview_no_activity": {
|
||||
"en": "No activity yet.", "ja": "まだアクティビティはありません。", "vi": "Chưa có hoạt động nào."},
|
||||
"monitoring.overview_resource_title": {
|
||||
"en": "Resources", "ja": "リソース", "vi": "Tài nguyên"},
|
||||
"monitoring.overview_res_cpu": {"en": "CPU", "ja": "CPU", "vi": "CPU"},
|
||||
"monitoring.overview_res_mem": {"en": "Memory", "ja": "メモリ", "vi": "Bộ nhớ"},
|
||||
"monitoring.overview_res_disk": {"en": "Disk I/O", "ja": "ディスク I/O", "vi": "Disk I/O"},
|
||||
"monitoring.overview_res_network": {"en": "Network", "ja": "ネットワーク", "vi": "Mạng"},
|
||||
# ---- model pricing list (Overview, beside the resource group) ----
|
||||
"monitoring.pricing_title": {"en": "Model pricing", "ja": "モデル料金", "vi": "Bảng giá model"},
|
||||
"monitoring.pricing_currency": {"en": "Currency", "ja": "通貨", "vi": "Tiền tệ"},
|
||||
"monitoring.pricing_import": {"en": "Import", "ja": "取込", "vi": "Nhập"},
|
||||
"monitoring.pricing_export": {"en": "Template", "ja": "テンプレート", "vi": "Mẫu"},
|
||||
"monitoring.pricing_add": {"en": "Add", "ja": "追加", "vi": "Thêm"},
|
||||
"monitoring.pricing_autolink": {"en": "Auto-link", "ja": "自動取得", "vi": "Tự lấy"},
|
||||
"monitoring.pricing_delete": {"en": "Delete", "ja": "削除", "vi": "Xóa"},
|
||||
"monitoring.pricing_add_prompt": {"en": "Model name:", "ja": "モデル名:", "vi": "Tên model:"},
|
||||
"monitoring.pricing_imported": {"en": "Imported {n} model prices.", "ja": "{n} 件の料金を取込。",
|
||||
"vi": "Đã nhập {n} dòng giá."},
|
||||
"monitoring.pricing_exported": {"en": "Price template exported.", "ja": "料金テンプレートを出力。",
|
||||
"vi": "Đã xuất mẫu bảng giá."},
|
||||
"monitoring.pricing_linked": {"en": "Linked {n} models from providers.",
|
||||
"ja": "プロバイダから {n} モデルを取得。",
|
||||
"vi": "Đã lấy {n} model từ provider."},
|
||||
"monitoring.pricing_col_model": {"en": "Model", "ja": "モデル", "vi": "Model"},
|
||||
"monitoring.pricing_col_context": {"en": "Context", "ja": "コンテキスト", "vi": "Context"},
|
||||
"monitoring.pricing_col_maxout": {"en": "Max output", "ja": "最大出力", "vi": "Max output"},
|
||||
"monitoring.pricing_col_input": {"en": "Input price", "ja": "入力単価", "vi": "Giá input"},
|
||||
"monitoring.pricing_col_output": {"en": "Output price", "ja": "出力単価", "vi": "Giá output"},
|
||||
"monitoring.overview_sandbox_details_title": {
|
||||
# One section now, holding both the sandbox facts and the permissions.
|
||||
"en": "Sandbox & Permissions", "ja": "サンドボックスと権限",
|
||||
"vi": "Sandbox & Quyền"},
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
"""Chuỗi hiển thị — phần composer.
|
||||
|
||||
Cắt ra từ ``i18n.py``: một dict 3.000 dòng không mở nổi để sửa một chữ.
|
||||
Cắt theo mốc phân đoạn có sẵn, nên mỗi cụm vẫn là các màn đi liền nhau.
|
||||
``i18n.py`` gộp tất cả lại thành ``STRINGS``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict
|
||||
|
||||
STRINGS: Dict[str, Dict[str, str]] = {
|
||||
"chatpanel.agent_tooltip": {
|
||||
"en": "Model/agent for THIS tab — independent of the other tab",
|
||||
"ja": "このタブ専用のモデル/エージェント(他のタブとは独立)",
|
||||
"vi": "Model/agent riêng cho tab này — độc lập với tab kia"},
|
||||
"chatpanel.agent_list_error": {
|
||||
"en": "Could not load the model list: {err}", "ja": "モデル一覧を読み込めませんでした: {err}",
|
||||
"vi": "Không tải được danh sách model: {err}"},
|
||||
"chatpanel.compress_btn": {"en": "Compress", "ja": "圧縮", "vi": "Nén"},
|
||||
"chatpanel.compress_tooltip": {
|
||||
"en": "Compress the conversation: trim old history to cut tokens (avoid exceeding the context limit)",
|
||||
"ja": "会話を圧縮:古い履歴を減らしてトークンを削減(コンテキスト上限超過を回避)",
|
||||
"vi": "Nén hội thoại: bỏ bớt lịch sử cũ để giảm token (tránh lỗi vượt giới hạn context)"},
|
||||
"chatpanel.files_header": {"en": "Files", "ja": "ファイル", "vi": "Files"},
|
||||
"chatpanel.collapse_files_tooltip": {
|
||||
"en": "Collapse the Files panel", "ja": "ファイルパネルを折りたたむ", "vi": "Thu gọn bảng Files"},
|
||||
"chatpanel.expand_files_tooltip": {
|
||||
"en": "Click to expand the Files panel", "ja": "クリックしてファイルパネルを展開",
|
||||
"vi": "Bấm để mở lại bảng Files"},
|
||||
"chatpanel.compress_busy": {
|
||||
"en": "Running — stop or wait before compressing.", "ja": "実行中です。停止するか完了を待ってから圧縮してください。",
|
||||
"vi": "Đang chạy — dừng hoặc đợi xong rồi hãy nén."},
|
||||
"chatpanel.compress_short": {
|
||||
"en": "Conversation is already short — no need to compress.",
|
||||
"ja": "会話はすでに短いため圧縮の必要はありません。",
|
||||
"vi": "Hội thoại đã ngắn — không cần nén."},
|
||||
"chatpanel.compress_done": {
|
||||
"en": "Compressed: dropped {cut} old messages, kept the last {keep} turns to cut tokens.",
|
||||
"ja": "圧縮しました:古いメッセージ{cut}件を削除し、直近{keep}ターンを保持してトークンを削減。",
|
||||
"vi": "Đã nén hội thoại: bỏ {cut} tin cũ, giữ {keep} lượt gần nhất để giảm token."},
|
||||
"chatpanel.compress_reduced": {
|
||||
"en": "Compressed to {pct}% of the original ({n} old messages digested).",
|
||||
"ja": "元の {pct}% まで圧縮(古いメッセージ {n} 件を要約)。",
|
||||
"vi": "Đã nén còn {pct}% so với ban đầu ({n} tin cũ được tóm gọn)."},
|
||||
"chatpanel.compress_digest_header": {
|
||||
"en": "Compressed summary of {n} earlier messages",
|
||||
"ja": "以前のメッセージ {n} 件の要約",
|
||||
"vi": "Tóm tắt nén của {n} tin nhắn trước đó"},
|
||||
"chatpanel.delete_confirm_title": {"en": "Delete message", "ja": "メッセージを削除", "vi": "Xóa tin nhắn"},
|
||||
"chatpanel.delete_confirm_files": {
|
||||
"en": "Delete this message and its {n} input/output file(s)?\n\n{preview}",
|
||||
"ja": "このメッセージと入出力ファイル{n}件を削除しますか?\n\n{preview}",
|
||||
"vi": "Xóa tin nhắn này và {n} tệp input/output của nó?\n\n{preview}"},
|
||||
"chatpanel.delete_confirm_plain": {"en": "Delete this message?", "ja": "このメッセージを削除しますか?", "vi": "Xóa tin nhắn này?"},
|
||||
"chatpanel.delete_done": {
|
||||
"en": "Message and its files deleted.", "ja": "メッセージとファイルを削除しました。",
|
||||
"vi": "Đã xóa tin nhắn và các tệp liên quan."},
|
||||
"chatpanel.working": {"en": "{name}: working…", "ja": "{name}: 処理中…", "vi": "{name}: đang xử lý…"},
|
||||
"chatpanel.done": {"en": "{name}: done.", "ja": "{name}: 完了。", "vi": "{name}: xong."},
|
||||
"chatpanel.failed": {"en": "{name}: error.", "ja": "{name}: エラー。", "vi": "{name}: lỗi."},
|
||||
"chatpanel.stopping": {"en": "{name}: stopping…", "ja": "{name}: 停止中…", "vi": "{name}: đang dừng…"},
|
||||
"chatpanel.attach_limit": {
|
||||
"en": "Max {n} attachments — extra files were skipped.",
|
||||
"ja": "添付は最大{n}件です。超過分はスキップされました。",
|
||||
"vi": "Tối đa {n} tệp đính kèm — bỏ qua phần dư."},
|
||||
"chatpanel.attached_hint": {"en": "Attached: {names}", "ja": "添付: {names}", "vi": "Đã đính kèm: {names}"},
|
||||
"chatpanel.skills_updated": {"en": "Skills updated.", "ja": "スキルを更新しました。", "vi": "Đã cập nhật skill."},
|
||||
"chatpanel.new_files_detected": {
|
||||
"en": "New file(s) detected in output folder: {names} ({n} file(s)). They will be auto-loaded as input on the next message.",
|
||||
"ja": "出力フォルダに新しいファイルを検出しました: {names} ({n}ファイル)。次のメッセージで自動的に入力として読み込まれます。",
|
||||
"vi": "Phát hiện tệp mới trong thư mục đầu ra: {names} ({n} tệp). Chúng sẽ được tự động tải làm dữ liệu đầu vào ở tin nhắn tiếp theo.",
|
||||
},
|
||||
# ---- composer.py -----------------------------------------------
|
||||
"composer.placeholder_default": {
|
||||
"en": "Type a message… (Enter to send, Shift+Enter for newline)",
|
||||
"ja": "メッセージを入力…(Enterで送信、Shift+Enterで改行)",
|
||||
"vi": "Nhập tin nhắn… (Enter để gửi, Shift+Enter xuống dòng)"},
|
||||
"composer.placeholder_cowork": {
|
||||
"en": "Type a request or attach a file to process… (Enter to send)",
|
||||
"ja": "依頼内容を入力するかファイルを添付…(Enterで送信)",
|
||||
"vi": "Nhập yêu cầu hoặc đính kèm tệp để xử lí… (Enter để gửi)"},
|
||||
"composer.placeholder_code": {
|
||||
"en": "Assign a task to the Code agent… (Enter to send)",
|
||||
"ja": "Code エージェントにタスクを指示…(Enterで送信)",
|
||||
"vi": "Giao việc cho Code agent… (Enter để gửi)"},
|
||||
"composer.queue_label": {"en": "Queue ({n})", "ja": "キュー ({n})", "vi": "Hàng đợi ({n})"},
|
||||
"composer.queue_tooltip": {
|
||||
"en": "Double-click to remove a queued message", "ja": "ダブルクリックでキューから削除",
|
||||
"vi": "Nhấp đúp để xoá một tin nhắn khỏi hàng đợi"},
|
||||
"composer.attachments_label": {"en": "Attachments ({n})", "ja": "添付ファイル ({n})", "vi": "Tệp đính kèm ({n})"},
|
||||
"composer.attachments_tooltip": {
|
||||
"en": "Click on a chip to remove a file added by mistake",
|
||||
"ja": "誤って追加したファイルは で削除できます",
|
||||
"vi": "Bấm trên thẻ để gỡ tệp đính kèm nhầm"},
|
||||
"composer.remove_tooltip": {
|
||||
"en": "Remove this file (added by mistake)", "ja": "このファイルを削除(誤って追加)",
|
||||
"vi": "Gỡ tệp này (đính kèm nhầm)"},
|
||||
"composer.attach_btn_tooltip": {
|
||||
"en": "Attach images or files (you can also paste or drag them in)",
|
||||
"ja": "画像やファイルを添付(貼り付け・ドラッグも可)",
|
||||
"vi": "Đính kèm ảnh hoặc tệp (có thể dán hoặc kéo-thả vào)"},
|
||||
"composer.send": {"en": "Send", "ja": "送信", "vi": "Gửi"},
|
||||
"composer.queue_btn": {"en": "Queue", "ja": "キューに追加", "vi": "Thêm vào hàng đợi"},
|
||||
"composer.stop": {"en": "Stop", "ja": "停止", "vi": "Dừng"},
|
||||
"composer.attach_dialog_title": {"en": "Attach files / images", "ja": "ファイル/画像を添付", "vi": "Đính kèm tệp / ảnh"},
|
||||
"composer.attach_dialog_filter": {
|
||||
"en": "Files (*.*);;Images (*.png *.jpg *.jpeg *.gif *.bmp *.webp)",
|
||||
"ja": "ファイル (*.*);;画像 (*.png *.jpg *.jpeg *.gif *.bmp *.webp)",
|
||||
"vi": "Tệp (*.*);;Ảnh (*.png *.jpg *.jpeg *.gif *.bmp *.webp)"},
|
||||
"composer.no_skills": {"en": " (no skills yet)", "ja": " (スキルはまだありません)", "vi": " (chưa có skill nào)"},
|
||||
"composer.no_agents": {"en": " (no agents found)", "ja": " (エージェントが見つかりません)", "vi": " (không tìm thấy agent)"},
|
||||
"composer.manage_skills": {"en": "Manage skills…", "ja": "スキルを管理…", "vi": "Quản lý skill…"},
|
||||
|
||||
# ---- schedule_task_tab.py / task_editor_dialog.py -------------------
|
||||
"schedtask.title": {"en": "Schedule Task", "ja": "Schedule Task", "vi": "Schedule Task"},
|
||||
"schedtask.view.kanban": {"en": "Kanban", "ja": "Kanban", "vi": "Kanban"},
|
||||
"schedtask.view.calendar": {"en": "Calendar", "ja": "カレンダー", "vi": "Lịch"},
|
||||
"schedtask.no_title": {"en": "(untitled)", "ja": "(無題)", "vi": "(chưa có tên)"},
|
||||
"schedtask.cal_today": {"en": "Today", "ja": "今日", "vi": "Hôm nay"},
|
||||
"schedtask.cal_prev": {"en": "Previous", "ja": "前へ", "vi": "Trước"},
|
||||
"schedtask.cal_next": {"en": "Next", "ja": "次へ", "vi": "Sau"},
|
||||
"schedtask.cal_gran.week": {"en": "Week", "ja": "週", "vi": "Tuần"},
|
||||
"schedtask.cal_gran.month": {"en": "Month", "ja": "月", "vi": "Tháng"},
|
||||
"schedtask.cal_gran.year": {"en": "Year", "ja": "年", "vi": "Năm"},
|
||||
"schedtask.cal_weekday.mon": {"en": "Mon", "ja": "月", "vi": "T2"},
|
||||
"schedtask.cal_weekday.tue": {"en": "Tue", "ja": "火", "vi": "T3"},
|
||||
"schedtask.cal_weekday.wed": {"en": "Wed", "ja": "水", "vi": "T4"},
|
||||
"schedtask.cal_weekday.thu": {"en": "Thu", "ja": "木", "vi": "T5"},
|
||||
"schedtask.cal_weekday.fri": {"en": "Fri", "ja": "金", "vi": "T6"},
|
||||
"schedtask.cal_weekday.sat": {"en": "Sat", "ja": "土", "vi": "T7"},
|
||||
"schedtask.cal_weekday.sun": {"en": "Sun", "ja": "日", "vi": "CN"},
|
||||
"schedtask.cal_month_count": {"en": "{month} — {n} task(s)", "ja": "{month} — {n} 件",
|
||||
"vi": "{month} — {n} task"},
|
||||
"schedtask.search_ph": {"en": "Search tasks…", "ja": "タスクを検索…", "vi": "Tìm task…"},
|
||||
"schedtask.filter_all": {"en": "All types", "ja": "すべての種類", "vi": "Mọi loại"},
|
||||
"schedtask.add_btn": {"en": "Add Task", "ja": "タスク追加", "vi": "Thêm Task"},
|
||||
"schedtask.ai_btn": {"en": "AI Create Task", "ja": "AIでタスク作成", "vi": "AI tạo Task"},
|
||||
"schedtask.ai_tooltip": {
|
||||
"en": "Describe what you want in natural language — AI proposes tasks/schedule/chain, you confirm before anything is created.",
|
||||
"ja": "自然文で説明すると、AIがタスク・スケジュール・チェーンを提案します。確認後に作成されます。",
|
||||
"vi": "Mô tả bằng ngôn ngữ tự nhiên — AI đề xuất task/lịch/chuỗi, bạn xác nhận rồi mới tạo."},
|
||||
"schedtask.no_tasks": {"en": "No tasks", "ja": "タスクなし", "vi": "No tasks"},
|
||||
"schedtask.no_schedule": {"en": "No schedule", "ja": "スケジュールなし", "vi": "Chưa đặt lịch"},
|
||||
"schedtask.last_success": {"en": "Last: Success", "ja": "前回: 成功", "vi": "Lần cuối: Thành công"},
|
||||
"schedtask.last_failed": {"en": "Last: Failed", "ja": "前回: 失敗", "vi": "Lần cuối: Lỗi"},
|
||||
"schedtask.last_never": {"en": "Last: not run", "ja": "前回: 未実行", "vi": "Lần cuối: chưa chạy"},
|
||||
"schedtask.status.backlog": {"en": "Backlog", "ja": "Backlog", "vi": "Backlog"},
|
||||
"schedtask.status.scheduled": {"en": "Scheduled", "ja": "Scheduled", "vi": "Scheduled"},
|
||||
"schedtask.status.running": {"en": "Running", "ja": "Running", "vi": "Running"},
|
||||
"schedtask.status.waiting_input": {"en": "Waiting Input", "ja": "Waiting Input", "vi": "Waiting Input"},
|
||||
"schedtask.status.done": {"en": "Done", "ja": "Done", "vi": "Done"},
|
||||
"schedtask.status.failed": {"en": "Failed", "ja": "Failed", "vi": "Failed"},
|
||||
"schedtask.status.paused": {"en": "Paused", "ja": "Paused", "vi": "Paused"},
|
||||
"schedtask.type.cowork": {"en": "Cowork", "ja": "Cowork", "vi": "Cowork"},
|
||||
"schedtask.type.co4e_code": {"en": "Code", "ja": "Code", "vi": "Code"},
|
||||
"schedtask.type.flow": {"en": "Flow", "ja": "Flow", "vi": "Flow"},
|
||||
"schedtask.type.script": {"en": "Script", "ja": "Script", "vi": "Script"},
|
||||
"schedtask.type.manual": {"en": "Manual", "ja": "Manual", "vi": "Manual"},
|
||||
"schedtask.priority.low": {"en": "Low", "ja": "低", "vi": "Thấp"},
|
||||
"schedtask.priority.medium": {"en": "Medium", "ja": "中", "vi": "Trung bình"},
|
||||
"schedtask.priority.high": {"en": "High", "ja": "高", "vi": "Cao"},
|
||||
"schedtask.priority.critical": {"en": "Critical", "ja": "最重要", "vi": "Khẩn cấp"},
|
||||
"schedtask.menu_run": {"en": "Run now", "ja": "今すぐ実行", "vi": "Chạy ngay"},
|
||||
"schedtask.menu_edit": {"en": "Edit task", "ja": "タスクを編集", "vi": "Sửa task"},
|
||||
"schedtask.menu_duplicate": {"en": "Duplicate task", "ja": "タスクを複製", "vi": "Nhân bản task"},
|
||||
"schedtask.menu_pause": {"en": "Pause", "ja": "一時停止", "vi": "Tạm dừng"},
|
||||
"schedtask.menu_resume": {"en": "Resume", "ja": "再開", "vi": "Tiếp tục"},
|
||||
"schedtask.menu_logs": {"en": "View logs", "ja": "ログを表示", "vi": "Xem log"},
|
||||
"schedtask.menu_history": {"en": "Run history…", "ja": "実行履歴…", "vi": "Lịch sử chạy…"},
|
||||
"schedtask.hist_hint": {
|
||||
"en": "Double-click a row to open that run's artifact folder.",
|
||||
"ja": "行をダブルクリックすると、その実行のフォルダを開きます。",
|
||||
"vi": "Double-click một dòng để mở thư mục artifact của lần chạy đó."},
|
||||
"schedtask.hist_col_time": {"en": "Finished at", "ja": "完了時刻", "vi": "Hoàn thành lúc"},
|
||||
"schedtask.hist_col_status": {"en": "Status", "ja": "状態", "vi": "Trạng thái"},
|
||||
"schedtask.hist_col_run": {"en": "Run ID", "ja": "実行ID", "vi": "Run ID"},
|
||||
"schedtask.hist_col_error": {"en": "Error", "ja": "エラー", "vi": "Lỗi"},
|
||||
"schedtask.menu_create_next": {
|
||||
"en": "Create next task from output", "ja": "出力から次タスクを作成",
|
||||
"vi": "Tạo task tiếp theo từ output"},
|
||||
"schedtask.menu_delete": {"en": "Delete task", "ja": "タスクを削除", "vi": "Xóa task"},
|
||||
"schedtask.delete_confirm": {"en": "Delete '{title}'?", "ja": "「{title}」を削除しますか?", "vi": "Xóa '{title}'?"},
|
||||
"schedtask.menu_delete_selected": {"en": "Delete {n} selected", "ja": "選択した{n}件を削除", "vi": "Xóa {n} task đã chọn"},
|
||||
"schedtask.delete_multi_confirm": {
|
||||
"en": "Delete {n} selected tasks? This cannot be undone.",
|
||||
"ja": "選択した{n}件のタスクを削除しますか?元に戻せません。",
|
||||
"vi": "Xóa {n} task đã chọn? Không thể hoàn tác."},
|
||||
"schedtask.msg_created": {"en": "Task created.", "ja": "タスクを作成しました。", "vi": "Đã tạo task."},
|
||||
"schedtask.msg_running": {"en": "Running: {title}", "ja": "実行中: {title}", "vi": "Đang chạy: {title}"},
|
||||
"schedtask.msg_manual_norun": {
|
||||
"en": "Manual tasks are for tracking only — they don't execute.",
|
||||
"ja": "Manualタスクは管理用のため実行されません。",
|
||||
"vi": "Task Manual chỉ để quản lý — không tự chạy."},
|
||||
"schedtask.msg_no_scheduler": {"en": "Scheduler not available.", "ja": "スケジューラーが利用できません。", "vi": "Scheduler chưa sẵn sàng."},
|
||||
"schedtask.msg_ai_created": {"en": "Created {n} task(s) from AI plan.", "ja": "AI提案から{n}件のタスクを作成しました。", "vi": "Đã tạo {n} task từ đề xuất AI."},
|
||||
"schedtask.no_runs_yet": {"en": "This task has not run yet.", "ja": "このタスクはまだ実行されていません。", "vi": "Task này chưa chạy lần nào."},
|
||||
"schedtask.next_of": {"en": "Next: {title}", "ja": "次: {title}", "vi": "Tiếp theo: {title}"},
|
||||
# editor
|
||||
"schedtask.editor_title_new": {"en": "Add Task", "ja": "タスク追加", "vi": "Thêm Task"},
|
||||
"schedtask.editor_title_edit": {"en": "Edit Task", "ja": "タスク編集", "vi": "Sửa Task"},
|
||||
"schedtask.f_title": {"en": "Title", "ja": "タイトル", "vi": "Tiêu đề"},
|
||||
"schedtask.f_desc": {"en": "Description", "ja": "説明", "vi": "Mô tả"},
|
||||
"schedtask.f_type": {"en": "Task type", "ja": "タスク種別", "vi": "Loại task"},
|
||||
"schedtask.f_workspace": {"en": "Workspace", "ja": "ワークスペース", "vi": "Workspace"},
|
||||
"schedtask.no_workspace": {"en": "— No workspace —", "ja": "— ワークスペースなし —", "vi": "— Không có workspace —"},
|
||||
"schedtask.f_agent": {"en": "Agent", "ja": "エージェント", "vi": "Agent"},
|
||||
"schedtask.no_agent": {"en": "— No agent preset —", "ja": "— エージェントなし —", "vi": "— Không dùng agent —"},
|
||||
"schedtask.f_provider": {"en": "Provider", "ja": "プロバイダー", "vi": "Provider"},
|
||||
"schedtask.provider_default": {
|
||||
"en": "— Default (Settings) —", "ja": "— 既定(設定)—", "vi": "— Mặc định (Settings) —"},
|
||||
"schedtask.f_model": {"en": "Model", "ja": "モデル", "vi": "Model"},
|
||||
"schedtask.model_placeholder": {
|
||||
"en": "Default model (leave blank to use Settings)",
|
||||
"ja": "既定のモデル(空欄で設定を使用)",
|
||||
"vi": "Model mặc định (để trống dùng Settings)"},
|
||||
"schedtask.load_models_tooltip": {
|
||||
"en": "Fetch this provider's available models",
|
||||
"ja": "このプロバイダーの利用可能なモデルを取得",
|
||||
"vi": "Tải danh sách model của provider này"},
|
||||
"schedtask.load_models_empty": {
|
||||
"en": "No models could be loaded. Check the provider/API key in Settings.",
|
||||
"ja": "モデルを取得できませんでした。設定のプロバイダー/APIキーを確認してください。",
|
||||
"vi": "Không tải được model nào. Kiểm tra provider/API key trong Settings."},
|
||||
"schedtask.f_skill": {"en": "Skill", "ja": "スキル", "vi": "Skill"},
|
||||
"schedtask.no_skill": {"en": "— No skill —", "ja": "— スキルなし —", "vi": "— Không dùng skill —"},
|
||||
"schedtask.hint_provider": {
|
||||
"en": "Which AI provider runs this task. Leave as Default to use the machine's Settings provider.",
|
||||
"ja": "このタスクを実行するAIプロバイダー。既定のままにすると設定のプロバイダーを使用します。",
|
||||
"vi": "Provider AI chạy task này. Để Mặc định để dùng provider trong Settings."},
|
||||
"schedtask.hint_model": {
|
||||
"en": "Model to run this task. Leave blank to use the provider's Settings model; click the button to load the real list.",
|
||||
"ja": "このタスクを実行するモデル。空欄で設定のモデルを使用。ボタンで実際の一覧を取得します。",
|
||||
"vi": "Model chạy task này. Để trống dùng model trong Settings; bấm nút để tải danh sách thực."},
|
||||
"schedtask.hint_skill": {
|
||||
"en": "Apply a saved skill's instructions to this task's run (its guidance is prepended to the prompt).",
|
||||
"ja": "保存済みスキルの指示をこのタスクの実行に適用します(プロンプトの先頭に追加されます)。",
|
||||
"vi": "Áp dụng hướng dẫn của một skill đã lưu vào lần chạy task này (được thêm vào đầu prompt)."},
|
||||
"schedtask.hint_workspace": {
|
||||
"en": "The project/workspace this task's agent runs in — its sandbox folder and shared instructions apply.",
|
||||
"ja": "このタスクのエージェントが実行されるプロジェクト/ワークスペース。そのサンドボックスフォルダと共有指示が適用されます。",
|
||||
"vi": "Project/workspace mà agent của task này sẽ chạy trong đó — áp dụng sandbox và hướng dẫn chung của project."},
|
||||
"schedtask.f_priority": {"en": "Priority", "ja": "優先度", "vi": "Độ ưu tiên"},
|
||||
"schedtask.f_status": {"en": "Status", "ja": "ステータス", "vi": "Trạng thái"},
|
||||
"schedtask.f_script": {"en": "Script command", "ja": "スクリプトコマンド", "vi": "Lệnh script"},
|
||||
"schedtask.script_placeholder": {
|
||||
"en": "(script tasks only) e.g. python report.py", "ja": "(Scriptタスクのみ)例: python report.py",
|
||||
"vi": "(chỉ task Script) vd: python report.py"},
|
||||
# The title/description block at the top of the Task editor had no name
|
||||
# either — needed once the index had to list it.
|
||||
"schedtask.g_basic": {"en": "Basics", "ja": "基本", "vi": "Thông tin chung"},
|
||||
# The three steps the editor is split into: what to do, when, and what it
|
||||
# connects to. Each holds the same group boxes as before.
|
||||
"schedtask.step_content": {"en": "Content", "ja": "内容", "vi": "Nội dung"},
|
||||
"schedtask.step_schedule": {"en": "Schedule", "ja": "スケジュール", "vi": "Lịch chạy"},
|
||||
"schedtask.step_link": {"en": "Links", "ja": "連携", "vi": "Liên kết"},
|
||||
"schedtask.g_schedule": {"en": "Schedule Setup", "ja": "スケジュール設定", "vi": "Thiết lập lịch chạy"},
|
||||
"schedtask.sched_enable": {"en": "Enable schedule", "ja": "スケジュールを有効化", "vi": "Bật lịch chạy"},
|
||||
"schedtask.f_run_at": {"en": "Run at", "ja": "実行日時", "vi": "Chạy lúc"},
|
||||
"schedtask.f_repeat": {"en": "Repeat", "ja": "繰り返し", "vi": "Lặp lại"},
|
||||
"schedtask.repeat.none": {"en": "None (one-time)", "ja": "なし(1回のみ)", "vi": "Không (chạy 1 lần)"},
|
||||
"schedtask.repeat.daily": {"en": "Daily", "ja": "毎日", "vi": "Hằng ngày"},
|
||||
"schedtask.repeat.weekly": {"en": "Weekly", "ja": "毎週", "vi": "Hằng tuần"},
|
||||
"schedtask.repeat.monthly": {"en": "Monthly", "ja": "毎月", "vi": "Hằng tháng"},
|
||||
"schedtask.repeat.cron": {"en": "Cron expression", "ja": "Cron式", "vi": "Cron expression"},
|
||||
"schedtask.f_task_mode": {"en": "Task type", "ja": "タスク種別", "vi": "Loại task"},
|
||||
# Run kind: an AI agent vs a saved Co4E flow + multi-format import
|
||||
"schedtask.f_run_kind": {"en": "Run", "ja": "実行対象", "vi": "Chạy"},
|
||||
"schedtask.kind_agent": {"en": "AI agent (Cowork)", "ja": "AIエージェント(Cowork)",
|
||||
"vi": "AI agent (Cowork)"},
|
||||
"schedtask.kind_flow": {"en": "Co4E flow", "ja": "Co4E フロー", "vi": "Co4E flow"},
|
||||
"schedtask.hint_run_kind": {
|
||||
"en": "AI agent = run one Cowork agent with the chosen model. Co4E flow = run a whole "
|
||||
"saved node-graph flow, step by step, in the sandbox.",
|
||||
"ja": "AIエージェント=選択モデルで Cowork エージェントを1つ実行。Co4E フロー=保存済みの"
|
||||
"ノードグラフ全体をサンドボックスで順に実行。",
|
||||
"vi": "AI agent = chạy một agent Cowork với model đã chọn. Co4E flow = chạy cả một flow "
|
||||
"node-graph đã lưu, tuần tự, trong sandbox."},
|
||||
"schedtask.f_flow": {"en": "Co4E flow", "ja": "Co4E フロー", "vi": "Co4E flow"},
|
||||
"schedtask.hint_flow": {
|
||||
"en": "Which saved Co4E flow this task runs (built-in or your own).",
|
||||
"ja": "このタスクが実行する保存済み Co4E フロー(組込み/自作)。",
|
||||
"vi": "Flow Co4E đã lưu mà task này sẽ chạy (có sẵn hoặc của bạn)."},
|
||||
"schedtask.flow_required": {
|
||||
"en": "Pick a Co4E flow to run (or switch Run to AI agent).",
|
||||
"ja": "実行する Co4E フローを選んでください(または実行対象を AI エージェントに)。",
|
||||
"vi": "Hãy chọn một flow Co4E để chạy (hoặc đổi Chạy sang AI agent)."},
|
||||
"schedtask.hint_task_mode": {
|
||||
"en": "Normal = runs once (or manually). Automation = a cronjob that repeats on a schedule "
|
||||
"(daily/weekly/monthly/cron). Switching to Automation reveals the recurrence options.",
|
||||
"ja": "通常=1回(または手動)実行。自動化=スケジュールで繰り返すCronジョブ(毎日/毎週/毎月/Cron)。"
|
||||
"自動化に切り替えると繰り返し設定が表示されます。",
|
||||
"vi": "Thông thường = chạy một lần (hoặc thủ công). Tự động = cronjob lặp theo lịch "
|
||||
"(ngày/tuần/tháng/cron). Chuyển sang Tự động sẽ hiện các tùy chọn lặp lại."},
|
||||
"schedtask.mode_normal": {
|
||||
"en": "Normal (one-time / manual)", "ja": "通常(1回 / 手動)",
|
||||
"vi": "Thông thường (một lần / thủ công)"},
|
||||
"schedtask.mode_automation": {
|
||||
"en": "Automation (cron / recurring)", "ja": "自動化(Cron / 繰り返し)",
|
||||
"vi": "Tự động (cronjob / lặp lại)"},
|
||||
"schedtask.f_cron": {"en": "Cron", "ja": "Cron", "vi": "Cron"},
|
||||
"schedtask.cron_sample_pick": {"en": "Sample ▾", "ja": "サンプル ▾", "vi": "Mẫu ▾"},
|
||||
"schedtask.cron_sample_tooltip": {
|
||||
"en": "Pick a ready-made schedule — it fills the cron box with correct syntax.",
|
||||
"ja": "定番スケジュールを選ぶと、正しい書式でCron欄に入力されます。",
|
||||
"vi": "Chọn một lịch mẫu — sẽ điền đúng cú pháp vào ô cron."},
|
||||
"schedtask.cron_s_weekday9": {"en": "Weekdays 9:00", "ja": "平日 9:00", "vi": "Ngày làm việc 9:00"},
|
||||
"schedtask.cron_s_daily8": {"en": "Every day 8:00", "ja": "毎日 8:00", "vi": "Mỗi ngày 8:00"},
|
||||
"schedtask.cron_s_weekly_mon": {"en": "Every Monday 9:00", "ja": "毎週月曜 9:00", "vi": "Thứ 2 hằng tuần 9:00"},
|
||||
"schedtask.cron_s_monthly1": {"en": "1st of month 9:00", "ja": "毎月1日 9:00", "vi": "Ngày 1 hằng tháng 9:00"},
|
||||
"schedtask.cron_s_every30m": {"en": "Every 30 minutes", "ja": "30分ごと", "vi": "Mỗi 30 phút"},
|
||||
"schedtask.cron_s_every2h": {"en": "Every 2 hours", "ja": "2時間ごと", "vi": "Mỗi 2 giờ"},
|
||||
"schedtask.cron_placeholder": {
|
||||
"en": "(repeat = Cron) e.g. 0 9 * * 1-5 — min hour day month weekday",
|
||||
"ja": "(繰り返し=Cron)例: 0 9 * * 1-5 — 分 時 日 月 曜日",
|
||||
"vi": "(khi lặp = Cron) vd: 0 9 * * 1-5 — phút giờ ngày tháng thứ"},
|
||||
"schedtask.cron_hint": {
|
||||
"en": "Only when Repeat = Cron. Fields: minute hour day-of-month month day-of-week "
|
||||
"(e.g. '0 9 * * 1-5' = 9:00 every weekday). Otherwise the run-time above is the "
|
||||
"daily/weekly/monthly notification time.",
|
||||
"ja": "繰り返し=Cronの場合のみ。書式: 分 時 日 月 曜日(例 '0 9 * * 1-5' = 平日9:00)。"
|
||||
"それ以外は上の実行時刻が毎日/毎週/毎月の通知時刻になります。",
|
||||
"vi": "Chỉ khi Lặp = Cron. Cú pháp: phút giờ ngày tháng thứ (vd '0 9 * * 1-5' = 9:00 các "
|
||||
"ngày trong tuần). Nếu không, giờ chạy ở trên là giờ thông báo hàng ngày/tuần/tháng."},
|
||||
"schedtask.cron_invalid": {
|
||||
"en": "Invalid cron expression: {err}", "ja": "Cron式が不正です: {err}",
|
||||
"vi": "Cron expression không hợp lệ: {err}"},
|
||||
"schedtask.cron_never_fires": {
|
||||
"en": "This cron expression never fires (within 2 years).",
|
||||
"ja": "このCron式は(2年以内に)一度も実行されません。",
|
||||
"vi": "Cron expression này không bao giờ chạy (trong vòng 2 năm)."},
|
||||
"schedtask.workdays_only": {"en": "Working days only (skip Sat/Sun)", "ja": "平日のみ(土日をスキップ)", "vi": "Chỉ ngày làm việc (bỏ T7/CN)"},
|
||||
"schedtask.skip_holidays": {
|
||||
"en": "Skip public holidays", "ja": "祝日をスキップ", "vi": "Bỏ qua ngày nghỉ lễ"},
|
||||
"schedtask.holiday_country": {"en": "Country:", "ja": "国:", "vi": "Quốc gia:"},
|
||||
"schedtask.f_notify": {"en": "Reminder", "ja": "リマインダー", "vi": "Nhắc nhở"},
|
||||
"schedtask.f_notify_email": {"en": "Send to", "ja": "送信先", "vi": "Gửi tới"},
|
||||
"schedtask.notify.none": {"en": "— No reminder —", "ja": "— リマインダーなし —", "vi": "— Không nhắc —"},
|
||||
"schedtask.notify.teams": {"en": "Teams (webhook)", "ja": "Teams(Webhook)", "vi": "Teams (webhook)"},
|
||||
"schedtask.notify.outlook": {
|
||||
"en": "Email via Outlook (this PC)", "ja": "Outlookでメール(このPC)",
|
||||
"vi": "Email qua Outlook (máy này)"},
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
"""Chuỗi hiển thị — phần cowork_tab.
|
||||
|
||||
Cắt ra từ ``i18n.py``: một dict 3.000 dòng không mở nổi để sửa một chữ.
|
||||
Cắt theo mốc phân đoạn có sẵn, nên mỗi cụm vẫn là các màn đi liền nhau.
|
||||
``i18n.py`` gộp tất cả lại thành ``STRINGS``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict
|
||||
|
||||
STRINGS: Dict[str, Dict[str, str]] = {
|
||||
"dashboard.estimated_note": {
|
||||
"en": "~{pct}% of turns are estimated (~4 chars/token) — the gateway didn't report exact usage.",
|
||||
"ja": "約{pct}%のターンは推定値(約4文字/トークン)です。",
|
||||
"vi": "~{pct}% lượt là ước tính (~4 ký tự/token) — gateway không trả về usage chính xác."},
|
||||
"dashboard.ai_analyze_btn": {"en": "AI analyze", "ja": "AI分析", "vi": "AI phân tích"},
|
||||
"dashboard.ai_analyzing": {"en": "Analyzing…", "ja": "分析中…", "vi": "Đang phân tích…"},
|
||||
"dashboard.ai_analyze_tooltip": {
|
||||
"en": "AI reviews the aggregated numbers (never your prompt contents) and suggests how to prompt better and spend fewer tokens.",
|
||||
"ja": "集計値のみをAIがレビューし(プロンプト内容は送信しません)、トークン削減のコツを提案します。",
|
||||
"vi": "AI xem các con số tổng hợp (không gửi nội dung prompt) và gợi ý cách viết prompt tốt hơn, tốn ít token hơn."},
|
||||
"dashboard.ai_advice_title": {
|
||||
"en": "AI recommendations", "ja": "AIの提案", "vi": "Khuyến nghị từ AI"},
|
||||
"dashboard.period_tooltip": {
|
||||
"en": "Time range for all numbers on this page.", "ja": "このページ全体の集計期間。",
|
||||
"vi": "Khoảng thời gian tính mọi con số trên trang này."},
|
||||
"dashboard.source_tooltip": {
|
||||
"en": "Filter by one task/session, or all.", "ja": "タスク/セッション単位で絞り込み。",
|
||||
"vi": "Lọc theo 1 task/phiên, hoặc tất cả."},
|
||||
"dashboard.currency_tooltip": {
|
||||
"en": "Display currency (rates: fixed USD→VND/JPY, editable in config).",
|
||||
"ja": "表示通貨(USD→VND/JPYの固定レート、configで変更可)。",
|
||||
"vi": "Tiền tệ hiển thị (tỉ giá USD→VND/JPY cố định, sửa được trong config)."},
|
||||
"dashboard.price_in_tooltip": {
|
||||
"en": "USD per 1M input tokens (your gateway's price).",
|
||||
"ja": "入力100万トークンあたりのUSD単価。", "vi": "USD cho 1 triệu token input (giá của gateway bạn dùng)."},
|
||||
"dashboard.price_out_tooltip": {
|
||||
"en": "USD per 1M output tokens.", "ja": "出力100万トークンあたりのUSD単価。",
|
||||
"vi": "USD cho 1 triệu token output."},
|
||||
"dashboard.price_cache_tooltip": {
|
||||
"en": "USD per 1M cached tokens.", "ja": "キャッシュ100万トークンあたりのUSD単価。",
|
||||
"vi": "USD cho 1 triệu token cache."},
|
||||
# ---- cowork_tab.py -------------------------------------------------
|
||||
"cowork.title": {"en": "Cowork", "ja": "Cowork", "vi": "Cowork"},
|
||||
"cowork.skills_btn": {"en": "Skills", "ja": "スキル", "vi": "Skills"},
|
||||
"cowork.skills_tooltip": {
|
||||
"en": "Add / manage skills the agent follows (or type /skill).",
|
||||
"ja": "エージェントが従うスキルを追加/管理(/skill と入力も可)。",
|
||||
"vi": "Thêm/quản lý skill mà agent tuân theo (hoặc gõ /skill)."},
|
||||
"cowork.new_chat": {"en": "New chat", "ja": "新しいチャット", "vi": "Cuộc trò chuyện mới"},
|
||||
"cowork.assistant_title": {"en": "Internal Agent", "ja": "内部エージェント", "vi": "Internal Agent"},
|
||||
"cowork.project_label": {"en": "{name}", "ja": "{name}", "vi": "{name}"},
|
||||
"cowork.project_tooltip": {
|
||||
"en": "This thread belongs to project “{name}” — its shared instructions and workspace apply. Manage projects in the Workspace screen.",
|
||||
"ja": "このスレッドはプロジェクト「{name}」に属します — 共有指示とワークスペースが適用されます。プロジェクトはワークスペース画面で管理できます。",
|
||||
"vi": "Thread này thuộc project “{name}” — instructions chung và workspace của project được áp dụng. Quản lý project trong màn hình Workspace.",
|
||||
},
|
||||
"cowork.pick_folder_btn": {"en": "Local folder…",
|
||||
"ja": "ローカルフォルダ…",
|
||||
"vi": "Thư mục Local…"},
|
||||
"cowork.pick_folder_tooltip": {
|
||||
"en": "Save Cowork's output directly into a folder you choose, instead of "
|
||||
"auto-creating a new session folder under Output.",
|
||||
"ja": "Output 配下に新しいセッションフォルダを自動作成する代わりに、選んだフォルダに直接保存します。",
|
||||
"vi": "Lưu output của Cowork trực tiếp vào thư mục bạn chọn, thay vì tự tạo "
|
||||
"folder phiên mới trong Output."},
|
||||
"cowork.pick_folder_title": {"en": "Choose the Cowork output folder",
|
||||
"ja": "Cowork の出力フォルダを選択",
|
||||
"vi": "Chọn thư mục output cho Cowork"},
|
||||
|
||||
# ---- code_tab.py -----------------------------------------------
|
||||
"code.title": {"en": "Code", "ja": "Code", "vi": "Code"},
|
||||
"code.files_header": {"en": "Files", "ja": "ファイル", "vi": "Files"},
|
||||
"code.collapse_file_panel": {
|
||||
"en": "Collapse the file panel", "ja": "ファイルパネルを折りたたむ", "vi": "Thu gọn bảng cây thư mục"},
|
||||
"code.expand_file_panel": {
|
||||
"en": "Click to expand the file panel", "ja": "クリックしてファイルパネルを展開",
|
||||
"vi": "Bấm để mở lại bảng cây thư mục"},
|
||||
"code.local_btn": {"en": "Local…", "ja": "ローカル…", "vi": "Local…"},
|
||||
"code.onedrive_btn": {"en": "OneDrive", "ja": "OneDrive", "vi": "OneDrive"},
|
||||
"code.onedrive_badge": {"en": "OneDrive", "ja": "OneDrive", "vi": "OneDrive"},
|
||||
"code.auto_run": {"en": "Auto-run", "ja": "自動実行", "vi": "Tự động"},
|
||||
"code.auto_run_tooltip": {
|
||||
"en": "On: agent writes files / runs commands automatically. Off: ask before each action.",
|
||||
"ja": "オン:エージェントが自動でファイル書き込み/コマンド実行。オフ:毎回確認します。",
|
||||
"vi": "Bật: agent tự ghi file/chạy lệnh. Tắt: hỏi xác nhận trước mỗi thao tác."},
|
||||
"code.skills_tooltip": {
|
||||
"en": "Add / set skills for the agent to follow (or type /skill).",
|
||||
"ja": "エージェントが従うスキルを追加/設定(/skill と入力も可)。",
|
||||
"vi": "Thêm/đặt skill mà agent tuân theo (hoặc gõ /skill)."},
|
||||
"code.flow_chk": {"en": "Flow", "ja": "フロー", "vi": "Flow"},
|
||||
"code.flow_chk_tooltip": {
|
||||
"en": "Enable the predefined Req→Demo flow feature (off by default).",
|
||||
"ja": "定義済みの Req→Demo フロー機能を有効化(初期値はオフ)。",
|
||||
"vi": "Bật tính năng Flow Req→Demo dựng sẵn (mặc định tắt)."},
|
||||
"code.flow_btn": {"en": "Flow Management", "ja": "Flow Management", "vi": "Flow Management"},
|
||||
"code.flow_btn_tooltip": {
|
||||
"en": "Build and run a multi-stage flow from requirement to demo.",
|
||||
"ja": "要件からデモまでの多段フローを作成・実行します。",
|
||||
"vi": "Xây dựng và chạy quy trình nhiều bước từ yêu cầu đến bản demo."},
|
||||
"code.new_session": {"en": "New session", "ja": "新しいセッション", "vi": "Phiên mới"},
|
||||
"code.cli_tooltip": {
|
||||
"en": "Open a terminal (CLI) at the current working folder",
|
||||
"ja": "現在の作業フォルダでターミナル(CLI)を開く",
|
||||
"vi": "Mở CLI (terminal) tại thư mục làm việc hiện tại"},
|
||||
"code.cli_not_found": {
|
||||
"en": "No terminal application was found on this system.",
|
||||
"ja": "このシステムにはターミナルアプリが見つかりませんでした。",
|
||||
"vi": "Không tìm thấy ứng dụng terminal nào trên máy này."},
|
||||
"code.skills_btn_count": {"en": "Skills ({n})", "ja": "スキル ({n})", "vi": "Skills ({n})"},
|
||||
"code.assistant_title": {"en": "Code agent", "ja": "Code エージェント", "vi": "Code agent"},
|
||||
"code.plan": {"en": "Plan", "ja": "プラン", "vi": "Plan"},
|
||||
"code.act": {"en": "Act", "ja": "実行", "vi": "Act"},
|
||||
"code.mode_toggle_tooltip": {
|
||||
"en": "Plan = analyze only (no file writes). Act = execute. Auto-switches to Act on gencode.",
|
||||
"ja": "Plan=分析のみ(書き込みなし)。Act=実行。コード生成指示で自動的に Act に切替。",
|
||||
"vi": "Plan = chỉ phân tích (không ghi file). Act = thực thi. Tự chuyển sang Act khi phát hiện yêu cầu sinh code."},
|
||||
"code.act_status": {"en": "Code: Act mode (executes).", "ja": "Code: Act モード(実行)。", "vi": "Code: chế độ Act (thực thi)."},
|
||||
"code.plan_status": {"en": "Code: Plan mode (analyze only).", "ja": "Code: Plan モード(分析のみ)。", "vi": "Code: chế độ Plan (chỉ phân tích)."},
|
||||
"code.cloud_sync_suffix": {"en": " — files sync to cloud", "ja": " — クラウドに同期", "vi": " — file sẽ đồng bộ lên cloud"},
|
||||
"code.mode_auto_status": {"en": "Code: Auto-run mode.", "ja": "Code: 自動実行モード。", "vi": "Code: chế độ Tự động."},
|
||||
"code.mode_confirm_status": {"en": "Code: Confirm mode.", "ja": "Code: 確認モード。", "vi": "Code: chế độ Xác nhận."},
|
||||
"code.pick_local_title": {"en": "Choose working folder (Local)", "ja": "作業フォルダを選択(ローカル)", "vi": "Chọn thư mục làm việc (Local)"},
|
||||
"code.pick_onedrive_title": {"en": "Choose a folder in OneDrive", "ja": "OneDrive 内のフォルダを選択", "vi": "Chọn thư mục trong OneDrive"},
|
||||
"code.onedrive_choose_folder": {
|
||||
"en": "Choose a folder in OneDrive…", "ja": "OneDrive 内のフォルダを選択…", "vi": "Chọn thư mục trong OneDrive…"},
|
||||
"code.no_onedrive": {"en": "No OneDrive detected", "ja": "OneDrive が見つかりません", "vi": "Không phát hiện OneDrive"},
|
||||
"code.flow_running": {
|
||||
"en": "Running flow '{name}' (Act) — {n} stages.",
|
||||
"ja": "フロー「{name}」を実行中(Act)— {n} ステージ。",
|
||||
"vi": "Đang chạy flow '{name}' (Act) — {n} bước."},
|
||||
|
||||
# ---- settings_dialog.py --------------------------------------------
|
||||
"settings.title": {"en": "Settings", "ja": "設定", "vi": "Cài đặt"},
|
||||
"settings.active_provider": {"en": "Active provider", "ja": "使用中のプロバイダー", "vi": "Nhà cung cấp đang dùng"},
|
||||
"settings.theme": {"en": "Theme", "ja": "テーマ", "vi": "Giao diện"},
|
||||
"settings.theme_dark": {"en": "Dark", "ja": "ダーク", "vi": "Tối"},
|
||||
"settings.theme_light": {"en": "Light", "ja": "ライト", "vi": "Sáng"},
|
||||
"settings.theme_system": {"en": "Auto (System)", "ja": "自動(システム)", "vi": "Tự động (theo hệ thống)"},
|
||||
"settings.language": {"en": "Language", "ja": "言語", "vi": "Ngôn ngữ"},
|
||||
"settings.tray_keep": {
|
||||
"en": "Keep running in the system tray when the window is closed",
|
||||
"ja": "ウィンドウを閉じてもシステムトレイで実行を継続",
|
||||
"vi": "Giữ chạy nền trong khay hệ thống khi đóng cửa sổ"},
|
||||
"settings.tray_notify": {
|
||||
"en": "Show a tray notification when a task finishes or fails",
|
||||
"ja": "タスク完了/失敗時にトレイ通知を表示",
|
||||
"vi": "Hiện thông báo khay hệ thống khi tác vụ xong hoặc lỗi"},
|
||||
"settings.group.openai": {"en": "OpenAI-compatible (Internal Gateway)", "ja": "OpenAI 互換(社内ゲートウェイ)", "vi": "OpenAI-compatible (Gateway nội bộ)"},
|
||||
"settings.group.anthropic": {"en": "Anthropic Claude", "ja": "Anthropic Claude", "vi": "Anthropic Claude"},
|
||||
# Name for the language/tray block at the top of Settings — it had none,
|
||||
# because until the index existed nothing had to refer to it.
|
||||
"settings.group.general": {"en": "General", "ja": "一般", "vi": "Chung"},
|
||||
"settings.group.provider": {"en": "AI Provider", "ja": "AI プロバイダー", "vi": "Nhà cung cấp AI"},
|
||||
"settings.group.parameter": {"en": "Parameter", "ja": "Parameter", "vi": "Parameter"},
|
||||
"settings.param_section_pricing": {
|
||||
"en": "Model pricing", "ja": "モデル価格", "vi": "Bảng giá model"},
|
||||
"settings.pricing_url_label": {
|
||||
"en": "Pricing reference link", "ja": "価格表の参考リンク", "vi": "Link bảng giá tham khảo"},
|
||||
"settings.pricing_url_placeholder": {
|
||||
"en": "https://… (the provider's public price list)",
|
||||
"ja": "https://…(プロバイダーの公開価格表)",
|
||||
"vi": "https://… (trang bảng giá công khai của provider)"},
|
||||
"settings.pricing_url_tooltip": {
|
||||
"en": "Shown as a reference link beside the Monitoring pricing table. Prices themselves are entered by hand in that table.",
|
||||
"ja": "監視画面の価格表の横に参考リンクとして表示されます。価格自体は表に手入力します。",
|
||||
"vi": "Hiển thị làm link tham khảo cạnh bảng giá trong Monitoring. Giá vẫn do Admin nhập tay vào bảng."},
|
||||
"settings.group.accounts": {
|
||||
"en": "Shared accounts folder", "ja": "共有アカウントフォルダー", "vi": "Thư mục tài khoản dùng chung"},
|
||||
"settings.accounts_dir_label": {"en": "Folder", "ja": "フォルダー", "vi": "Thư mục"},
|
||||
"settings.accounts_dir_placeholder": {
|
||||
"en": "OneDrive/network folder holding the shared accounts & groups",
|
||||
"ja": "アカウント/グループを保存する OneDrive・共有フォルダー",
|
||||
"vi": "Thư mục OneDrive/mạng chứa danh sách tài khoản & nhóm dùng chung"},
|
||||
"settings.accounts_dir_hint": {
|
||||
"en": "Where accounts, groups and shared telemetry live. Every machine must point at the SAME folder.",
|
||||
"ja": "アカウント・グループ・共有テレメトリの保存先。全マシンで同じフォルダーを指定してください。",
|
||||
"vi": "Nơi lưu tài khoản, nhóm và telemetry dùng chung. Mọi máy phải trỏ về CÙNG một thư mục."},
|
||||
"settings.accounts_dir_admin_only": {
|
||||
"en": "Only an Admin can change this folder.",
|
||||
"ja": "このフォルダーを変更できるのは管理者のみです。",
|
||||
"vi": "Chỉ Admin mới thay đổi được thư mục này."},
|
||||
"settings.group.monitoring_visibility": {
|
||||
"en": "Monitoring tab visibility (Sub-admin)", "ja": "モニタリングタブの表示(サブ管理者)",
|
||||
"vi": "Hiển thị tab Monitoring (Sub-admin)"},
|
||||
"settings.mv_security_events": {"en": "Security Events", "ja": "セキュリティイベント",
|
||||
"vi": "Security Events"},
|
||||
"settings.mv_mcp_history": {"en": "MCP Call History", "ja": "MCP 呼び出し履歴", "vi": "MCP Call History"},
|
||||
"settings.mv_action_logs": {"en": "Action Logs", "ja": "アクションログ", "vi": "Action Logs"},
|
||||
"settings.mv_agent_status": {"en": "Agent Status", "ja": "エージェント状態", "vi": "Trạng thái Agent"},
|
||||
"settings.mv_hint": {
|
||||
"en": "Admin always sees every Monitoring tab. Turn one off here to hide it from Sub-admin too (it stays available to Admin).",
|
||||
"ja": "管理者は常にすべてのタブを見られます。ここでオフにすると、そのタブはサブ管理者からも隠されます(管理者には影響しません)。",
|
||||
"vi": "Admin luôn thấy mọi tab Monitoring. Tắt một mục ở đây sẽ ẩn tab đó với Sub-admin (Admin vẫn thấy như thường)."},
|
||||
"settings.sec_unlock_user_placeholder": {
|
||||
"en": "Admin account", "ja": "管理者アカウント", "vi": "Tài khoản admin"},
|
||||
"settings.sec_unlock_code_placeholder": {
|
||||
"en": "Access code", "ja": "アクセスコード", "vi": "Mã truy cập"},
|
||||
"settings.sec_unlock_btn": {"en": "Unlock", "ja": "ロック解除", "vi": "Mở khóa"},
|
||||
"settings.sec_locked_hint": {
|
||||
"en": "Locked — enter an Admin account + access code to change these settings.",
|
||||
"ja": "ロック中 — 変更するには管理者アカウントとアクセスコードを入力してください。",
|
||||
"vi": "Đang khóa — nhập tài khoản Admin + mã truy cập để thay đổi các thiết lập này."},
|
||||
"settings.sec_unlocked_hint": {
|
||||
"en": "Unlocked — changes will be saved; the group locks again after Save.",
|
||||
"ja": "ロック解除中 — 保存後に再びロックされます。",
|
||||
"vi": "Đã mở khóa — thay đổi sẽ được lưu; nhóm sẽ tự khóa lại sau khi Save."},
|
||||
"settings.sec_unlock_failed": {
|
||||
"en": "Not an Admin account (or wrong code / accounts folder unreachable).",
|
||||
"ja": "管理者アカウントではありません(またはコード誤り・フォルダー未接続)。",
|
||||
"vi": "Không phải tài khoản Admin (hoặc sai mã / không truy cập được thư mục tài khoản)."},
|
||||
"settings.sec_no_lock_hint": {
|
||||
"en": "No shared accounts folder configured yet — the group is editable without an admin unlock.",
|
||||
"ja": "共有アカウントフォルダー未設定のため、ロックなしで編集できます。",
|
||||
"vi": "Chưa cấu hình thư mục tài khoản dùng chung — nhóm này đang chỉnh sửa được mà không cần mở khóa."},
|
||||
"settings.base_url": {"en": "Base URL", "ja": "ベース URL", "vi": "Base URL"},
|
||||
"settings.api_key": {"en": "API Key", "ja": "API キー", "vi": "API Key"},
|
||||
"settings.model": {"en": "Model", "ja": "モデル", "vi": "Model"},
|
||||
"settings.load": {"en": "Load", "ja": "読み込み", "vi": "Tải"},
|
||||
"settings.load_tooltip": {
|
||||
"en": "Fetch the available models/agents from this provider",
|
||||
"ja": "このプロバイダーから利用可能なモデル/エージェントを取得",
|
||||
"vi": "Lấy danh sách model/agent khả dụng từ nhà cung cấp này"},
|
||||
"settings.group.teams": {"en": "Microsoft Teams", "ja": "Microsoft Teams", "vi": "Microsoft Teams"},
|
||||
"settings.teams_webhook": {"en": "Webhook URL", "ja": "Webhook URL", "vi": "Webhook URL"},
|
||||
"settings.teams_webhook_placeholder": {
|
||||
"en": "https://… (Workflows or Incoming Webhook URL)",
|
||||
"ja": "https://…(Workflows または Incoming Webhook の URL)",
|
||||
"vi": "https://… (URL của Workflows hoặc Incoming Webhook)"},
|
||||
"settings.teams_test": {"en": "Test", "ja": "テスト", "vi": "Kiểm tra"},
|
||||
"settings.teams_notify": {
|
||||
"en": "Auto-send to Teams when a task completes", "ja": "タスク完了時に Teams へ自動送信",
|
||||
"vi": "Tự động gửi sang Teams khi tác vụ hoàn thành"},
|
||||
"settings.teams_hint": {
|
||||
"en": ("Get a webhook: Teams channel → ⋯ → Connectors → Incoming Webhook, "
|
||||
"OR Power Automate → 'When a HTTP request is received' → 'Post message in a chat or channel'. "
|
||||
"URL must contain logic.azure.com or webhook.office.com."),
|
||||
"ja": ("Webhook の取得: Teams チャンネル → ⋯ → コネクタ → Incoming Webhook、"
|
||||
"または Power Automate → 'HTTP要求の受信時' → 'チャットまたはチャネルにメッセージを投稿'。"
|
||||
"URL には logic.azure.com か webhook.office.com を含める必要があります。"),
|
||||
"vi": ("Lấy webhook: kênh Teams → ⋯ → Connectors → Incoming Webhook, "
|
||||
"HOẶC Power Automate → 'When a HTTP request is received' → 'Post message in a chat or channel'. "
|
||||
"URL phải chứa logic.azure.com hoặc webhook.office.com.")},
|
||||
"settings.group.ms365": {
|
||||
"en": "Microsoft 365 connections", "ja": "Microsoft 365 連携",
|
||||
"vi": "Kết nối Microsoft 365"},
|
||||
"settings.ms365_unlock_code": {"en": "Unlock code", "ja": "解除コード", "vi": "Mã mở khóa"},
|
||||
"settings.ms365_unlock_placeholder": {
|
||||
"en": "Enter the unlock code", "ja": "解除コードを入力",
|
||||
"vi": "Nhập mã để mở khóa"},
|
||||
"settings.ms365_unlock_btn": {"en": "Unlock", "ja": "解除", "vi": "Mở khóa"},
|
||||
"settings.ms365_locked_hint": {
|
||||
"en": "Locked — enter the unlock code above to edit this section.",
|
||||
"ja": "ロック中 — このセクションを編集するには上の解除コードを入力してください。",
|
||||
"vi": "Đang khóa — nhập mã ở trên để chỉnh sửa mục này."},
|
||||
"settings.ms365_unlocked_hint": {
|
||||
"en": "Unlocked — remember to click Save; this section re-locks automatically afterward.",
|
||||
"ja": "解除しました — 保存を忘れずに。保存後は自動的に再ロックされます。",
|
||||
"vi": "Đã mở khóa — nhớ bấm Save; mục này sẽ tự khóa lại ngay sau đó."},
|
||||
"settings.ms365_wrong_code": {
|
||||
"en": "Wrong code.", "ja": "コードが違います。", "vi": "Mã không đúng."},
|
||||
"settings.ms365_connector.outlook": {"en": "Outlook", "ja": "Outlook", "vi": "Outlook"},
|
||||
"settings.ms365_connector.teams": {"en": "Teams", "ja": "Teams", "vi": "Teams"},
|
||||
"settings.ms365_connector.onedrive": {"en": "OneDrive", "ja": "OneDrive", "vi": "OneDrive"},
|
||||
"settings.ms365_connector.sharepoint": {"en": "SharePoint", "ja": "SharePoint", "vi": "SharePoint"},
|
||||
"settings.ms365_connector.meeting_transcript": {
|
||||
"en": "Meeting transcript", "ja": "会議の文字起こし", "vi": "Meeting transcript"},
|
||||
"settings.ms365_allow_internet": {
|
||||
"en": "Allow external internet access", "ja": "外部インターネットアクセスを許可",
|
||||
"vi": "Cho phép truy cập Internet bên ngoài"},
|
||||
"settings.ms365_internet_off_hint": {
|
||||
"en": ("External internet access is OFF — every connector was turned off to avoid "
|
||||
"leaking data outside. Turn it back on, then re-tick the connectors you want."),
|
||||
"ja": ("外部インターネットアクセスがオフです — データが外部に漏れないよう、すべてのコネクタ"
|
||||
"がオフになりました。再度オンにしてから、必要なコネクタを選び直してください。"),
|
||||
"vi": ("Đã tắt truy cập Internet bên ngoài — mọi connector đã tự tắt để tránh rò rỉ "
|
||||
"thông tin ra ngoài. Bật lại rồi tick lại từng connector muốn dùng.")},
|
||||
"settings.ms365_signin_btn": {"en": "Sign in with Microsoft", "ja": "Microsoft でサインイン",
|
||||
"vi": "Đăng nhập Microsoft"},
|
||||
"settings.ms365_signout_btn": {"en": "Sign out", "ja": "サインアウト", "vi": "Đăng xuất"},
|
||||
"settings.ms365_not_signed_in": {
|
||||
"en": "Not signed in to Microsoft 365.", "ja": "Microsoft 365 にサインインしていません。",
|
||||
"vi": "Chưa đăng nhập Microsoft 365."},
|
||||
"settings.ms365_signed_in_as": {
|
||||
"en": "Signed in as {user}", "ja": "{user} としてサインイン中",
|
||||
"vi": "Đã đăng nhập với {user}"},
|
||||
"settings.ms365_missing_ids": {
|
||||
"en": "Enter the Tenant ID and Client ID first.", "ja": "先に Tenant ID と Client ID を入力してください。",
|
||||
"vi": "Hãy nhập Tenant ID và Client ID trước."},
|
||||
"settings.ms365_signing_in": {
|
||||
"en": "Starting sign-in…", "ja": "サインインを開始しています…", "vi": "Đang bắt đầu đăng nhập…"},
|
||||
"settings.ms365_signin_failed": {
|
||||
"en": "Sign-in failed: {err}", "ja": "サインインに失敗しました: {err}",
|
||||
"vi": "Đăng nhập thất bại: {err}"},
|
||||
"settings.ms365_teams_link_label": {
|
||||
"en": "Or just paste a Teams channel/chat link — no ID needed:",
|
||||
"ja": "または Teams のチャネル/チャットのリンクを貼り付けるだけ — ID は不要です:",
|
||||
"vi": "Hoặc chỉ cần paste link kênh/chat Teams — không cần ID:"},
|
||||
"settings.ms365_teams_link_placeholder": {
|
||||
"en": "Paste a link from Teams ('Get link to channel' or a message's 'Copy link')",
|
||||
"ja": "Teams のリンクを貼り付け(「チャネルへのリンクを取得」またはメッセージの「リンクをコピー」)",
|
||||
"vi": "Dán link từ Teams ('Get link to channel' hoặc 'Copy link' của 1 tin nhắn)"},
|
||||
"settings.ms365_teams_connect_btn": {"en": "Connect", "ja": "接続", "vi": "Kết nối"},
|
||||
"settings.ms365_teams_not_connected": {
|
||||
"en": "No Teams chat/channel connected yet.", "ja": "Teams のチャット/チャネルはまだ接続されていません。",
|
||||
"vi": "Chưa kết nối chat/kênh Teams nào."},
|
||||
"settings.ms365_teams_connected_channel": {
|
||||
"en": "Connected to a Teams channel.", "ja": "Teams のチャネルに接続済みです。",
|
||||
"vi": "Đã kết nối vào một kênh Teams."},
|
||||
"settings.ms365_teams_connected_chat": {
|
||||
"en": "Connected to a Teams chat.", "ja": "Teams のチャットに接続済みです。",
|
||||
"vi": "Đã kết nối vào một đoạn chat Teams."},
|
||||
"settings.ms365_teams_link_missing": {
|
||||
"en": "Paste a Teams link first.", "ja": "先に Teams のリンクを貼り付けてください。",
|
||||
"vi": "Hãy dán link Teams trước."},
|
||||
"settings.ms365_teams_connecting": {
|
||||
"en": "Connecting…", "ja": "接続しています…", "vi": "Đang kết nối…"},
|
||||
"settings.ms365_teams_connect_failed": {
|
||||
"en": "Connect failed: {err}", "ja": "接続に失敗しました: {err}",
|
||||
"vi": "Kết nối thất bại: {err}"},
|
||||
"settings.ms365_teams_intro_message": {
|
||||
"en": "Hi, I'm the Cowork agent — just connected to this chat/channel.",
|
||||
"ja": "こんにちは、Cowork エージェントです — このチャット/チャネルに接続しました。",
|
||||
"vi": "Xin chào, mình là Cowork agent — vừa kết nối vào chat/kênh này."},
|
||||
"settings.group.agent_security": {
|
||||
"en": "Agent Security (AI)", "ja": "エージェント セキュリティ(AI)",
|
||||
"vi": "Agent Security (AI)"},
|
||||
"settings.group.sandbox": {
|
||||
"en": "Sandbox Security Layer", "ja": "サンドボックス セキュリティ層",
|
||||
"vi": "Sandbox Security Layer"},
|
||||
"settings.sandbox_confirm_commands": {
|
||||
"en": "Confirm before Cowork runs a command",
|
||||
"ja": "Cowork がコマンドを実行する前に確認する",
|
||||
"vi": "Xác nhận trước khi Cowork chạy lệnh"},
|
||||
"settings.sandbox_confirm_commands_tooltip": {
|
||||
"en": ("Shows an Approve/Reject dialog before run_command/install_package "
|
||||
"executes in Cowork — off by default (auto-run), same as before."),
|
||||
"ja": "Cowork で run_command/install_package を実行する前に承認/拒否ダイアログを表示します — "
|
||||
"デフォルトはオフ(自動実行)で、これまでと同じです。",
|
||||
"vi": "Hiện hộp thoại Duyệt/Từ chối trước khi Cowork chạy run_command/install_package — "
|
||||
"mặc định tắt (tự chạy), giống hành vi cũ."},
|
||||
}
|
||||
+342
@@ -0,0 +1,342 @@
|
||||
"""Chuỗi hiển thị — phần hint.
|
||||
|
||||
Cắt ra từ ``i18n.py``: một dict 3.000 dòng không mở nổi để sửa một chữ.
|
||||
Cắt theo mốc phân đoạn có sẵn, nên mỗi cụm vẫn là các màn đi liền nhau.
|
||||
``i18n.py`` gộp tất cả lại thành ``STRINGS``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict
|
||||
|
||||
STRINGS: Dict[str, Dict[str, str]] = {
|
||||
"schedtask.notify_email_placeholder": {
|
||||
"en": "recipient@example.com (comma-separated)",
|
||||
"ja": "recipient@example.com(カンマ区切り)",
|
||||
"vi": "nguoinhan@example.com (cách nhau dấu phẩy)"},
|
||||
"schedtask.notify_hint": {
|
||||
"en": "When the scheduled/cron task finishes, send a reminder. Teams uses the webhook "
|
||||
"from Settings; Outlook sends from your signed-in Outlook desktop app — no login needed.",
|
||||
"ja": "スケジュール/Cronタスク完了時にリマインダーを送信。Teamsは設定のWebhookを使用、"
|
||||
"Outlookはサインイン済みのOutlookデスクトップから送信(ログイン不要)。",
|
||||
"vi": "Khi task theo lịch/cron chạy xong sẽ gửi nhắc. Teams dùng webhook trong Settings; "
|
||||
"Outlook gửi từ ứng dụng Outlook đã đăng nhập trên máy — không cần đăng nhập lại."},
|
||||
"schedtask.notify_need_email": {
|
||||
"en": "Enter a recipient address for the Outlook reminder.",
|
||||
"ja": "Outlookリマインダーの送信先アドレスを入力してください。",
|
||||
"vi": "Hãy nhập địa chỉ người nhận cho nhắc nhở qua Outlook."},
|
||||
"schedtask.notify_need_webhook": {
|
||||
"en": "Teams reminder needs a webhook URL — set it in Settings → Parameter first.",
|
||||
"ja": "TeamsリマインダーにはWebhook URLが必要です。先に設定→パラメータで設定してください。",
|
||||
"vi": "Nhắc qua Teams cần webhook URL — hãy đặt trong Settings → Parameter trước."},
|
||||
"schedtask.tz_local_note": {
|
||||
"en": "Times use this machine's local timezone.", "ja": "時刻はこのPCのローカルタイムゾーンです。",
|
||||
"vi": "Giờ dùng múi giờ local của máy này."},
|
||||
"schedtask.g_flow": {"en": "Flow Setup", "ja": "フロー設定", "vi": "Thiết lập Flow"},
|
||||
"schedtask.flow_hint": {
|
||||
"en": "(Flow tasks only) Steps run in order; each step's output feeds the next step's input.",
|
||||
"ja": "(Flowタスクのみ)ステップは順番に実行され、前ステップの出力が次の入力になります。",
|
||||
"vi": "(Chỉ task Flow) Các bước chạy tuần tự; output bước trước nối vào input bước sau."},
|
||||
"schedtask.flow_template": {"en": "Code template:", "ja": "Codeテンプレート:", "vi": "Template Code:"},
|
||||
"schedtask.import_flow_btn": {"en": "Import steps", "ja": "ステップ取込", "vi": "Nhập các bước"},
|
||||
"schedtask.flow_template_empty": {
|
||||
"en": "The selected template has no steps.", "ja": "選択したテンプレートにステップがありません。",
|
||||
"vi": "Template đã chọn không có bước nào."},
|
||||
"schedtask.step_name_ph": {"en": "Step name", "ja": "ステップ名", "vi": "Tên bước"},
|
||||
"schedtask.step_prompt_ph": {"en": "Prompt / command", "ja": "プロンプト/コマンド", "vi": "Prompt / lệnh"},
|
||||
"schedtask.stepexec.cowork": {"en": "Cowork", "ja": "Cowork", "vi": "Cowork"},
|
||||
"schedtask.stepexec.co4e": {"en": "Code", "ja": "Code", "vi": "Code"},
|
||||
"schedtask.stepexec.script": {"en": "Script", "ja": "Script", "vi": "Script"},
|
||||
"schedtask.stepexec.manual": {"en": "Manual", "ja": "Manual", "vi": "Manual"},
|
||||
"schedtask.del_step_tooltip": {"en": "Delete the selected step", "ja": "選択したステップを削除",
|
||||
"vi": "Xóa bước đang chọn"},
|
||||
"schedtask.guide_tooltip": {
|
||||
"en": "Open the Schedule Task user guide", "ja": "Schedule Task の使い方ガイドを開く",
|
||||
"vi": "Mở hướng dẫn sử dụng Schedule Task"},
|
||||
"schedtask.guide_missing": {
|
||||
"en": "Guide file not found (docs/schedule_task_user_guide.md).",
|
||||
"ja": "ガイドファイルが見つかりません (docs/schedule_task_user_guide.md)。",
|
||||
"vi": "Không tìm thấy file hướng dẫn (docs/schedule_task_user_guide.md)."},
|
||||
"schedtask.msg_set_schedule": {
|
||||
"en": "Set a run time so this task can actually run on schedule.",
|
||||
"ja": "実行時刻を設定するとスケジュール実行されます。",
|
||||
"vi": "Hãy đặt giờ chạy để task này thực sự chạy theo lịch."},
|
||||
# ---- hint tooltips (hover help) --------------------------------------
|
||||
"schedtask.add_tooltip": {
|
||||
"en": "Create a new task with full options (schedule, input, dependencies…).",
|
||||
"ja": "新しいタスクを作成(スケジュール・入力・依存など全設定)。",
|
||||
"vi": "Tạo task mới với đầy đủ tuỳ chọn (lịch, input, phụ thuộc…)."},
|
||||
"schedtask.search_tooltip": {
|
||||
"en": "Filter cards by title/description.", "ja": "タイトル/説明でカードを絞り込み。",
|
||||
"vi": "Lọc card theo tiêu đề/mô tả."},
|
||||
"schedtask.filter_tooltip": {
|
||||
"en": "Show only one task type.", "ja": "1つのタスク種別のみ表示。",
|
||||
"vi": "Chỉ hiện một loại task."},
|
||||
"schedtask.col_tip.backlog": {
|
||||
"en": "New tasks with no schedule yet. Drag a card here to shelve it.",
|
||||
"ja": "未スケジュールの新規タスク。", "vi": "Task mới, chưa đặt lịch. Kéo card vào đây để cất lại."},
|
||||
"schedtask.col_tip.scheduled": {
|
||||
"en": "On the calendar — runs automatically at its time. Drop a card here to schedule it.",
|
||||
"ja": "スケジュール済み — 時刻になると自動実行。", "vi": "Đã lên lịch — tự chạy khi đến giờ. Thả card vào đây để đặt lịch."},
|
||||
"schedtask.col_tip.running": {
|
||||
"en": "Currently executing. Drop a card here to RUN it immediately.",
|
||||
"ja": "実行中。ここにドロップすると即実行します。", "vi": "Đang chạy. Thả card vào đây để CHẠY NGAY."},
|
||||
"schedtask.col_tip.waiting_input": {
|
||||
"en": "Waiting: needs your Run-now approval, or its prerequisite tasks aren't Done yet.",
|
||||
"ja": "待機中: 手動承認待ち、または前提タスクが未完了。",
|
||||
"vi": "Đang chờ: cần bạn bấm Chạy ngay (phê duyệt), hoặc các task phụ thuộc chưa Done."},
|
||||
"schedtask.col_tip.done": {
|
||||
"en": "Finished successfully. Drop a card here to mark it done by hand.",
|
||||
"ja": "完了。ここにドロップすると手動で完了扱いにします。",
|
||||
"vi": "Đã xong. Thả card vào đây để tự đánh dấu hoàn thành."},
|
||||
"schedtask.col_tip.failed": {
|
||||
"en": "Last run errored — right-click → Run history to see why.",
|
||||
"ja": "前回失敗 — 右クリック→実行履歴で原因を確認。",
|
||||
"vi": "Lần chạy cuối bị lỗi — chuột phải → Lịch sử chạy để xem lý do."},
|
||||
"schedtask.col_tip.paused": {
|
||||
"en": "Paused: never auto-runs and is skipped by chains until resumed.",
|
||||
"ja": "一時停止中: 再開まで自動実行されず、チェーンでもスキップされます。",
|
||||
"vi": "Tạm dừng: không tự chạy và bị chuỗi bỏ qua cho tới khi tiếp tục."},
|
||||
"schedtask.hint_type": {
|
||||
"en": "Cowork = documents/answers · Code = coding agent · Script = shell command · Flow = multi-step · Manual = tracking only.",
|
||||
"ja": "Cowork=文書/回答 · Code=コーディング · Script=コマンド · Flow=複数ステップ · Manual=管理のみ。",
|
||||
"vi": "Cowork = tài liệu/trả lời · Code = agent code · Script = lệnh shell · Flow = nhiều bước · Manual = chỉ quản lý."},
|
||||
"schedtask.hint_status": {
|
||||
"en": "Current Kanban lane. Usually managed automatically by the scheduler.",
|
||||
"ja": "現在のKanbanレーン。通常はスケジューラーが自動管理。",
|
||||
"vi": "Cột Kanban hiện tại. Thường được scheduler tự quản lý."},
|
||||
"schedtask.hint_script": {
|
||||
"en": "Shell command to run (Script tasks). Runs in the task's artifact folder with a timeout.",
|
||||
"ja": "実行するシェルコマンド(Scriptタスク)。", "vi": "Lệnh shell sẽ chạy (task Script), trong thư mục artifact riêng, có timeout."},
|
||||
"schedtask.hint_sched_enable": {
|
||||
"en": "Off = the task never runs by itself.", "ja": "OFF = 自動実行されません。",
|
||||
"vi": "Tắt = task không bao giờ tự chạy."},
|
||||
"schedtask.hint_run_at": {
|
||||
"en": "First/next run time (this machine's local time).",
|
||||
"ja": "初回/次回の実行時刻(ローカル時刻)。", "vi": "Giờ chạy đầu/kế tiếp (giờ local của máy)."},
|
||||
"schedtask.hint_repeat": {
|
||||
"en": "After a successful run, the schedule rolls to the next occurrence automatically.",
|
||||
"ja": "成功後、次回分へ自動的に繰り越します。",
|
||||
"vi": "Sau khi chạy thành công, lịch tự dời sang kỳ kế tiếp."},
|
||||
"schedtask.hint_cron": {
|
||||
"en": "5 fields: minute hour day month weekday. E.g. '0 9 * * 1-5' = 9:00 on weekdays.",
|
||||
"ja": "5項目: 分 時 日 月 曜日。例 '0 9 * * 1-5' = 平日9時。",
|
||||
"vi": "5 trường: phút giờ ngày tháng thứ. VD '0 9 * * 1-5' = 9h các ngày thường."},
|
||||
"schedtask.hint_workdays": {
|
||||
"en": "Runs landing on Sat/Sun are pushed to the next working day.",
|
||||
"ja": "土日に当たる回は翌営業日に繰り越し。", "vi": "Lịch rơi vào T7/CN sẽ dời sang ngày làm việc kế."},
|
||||
"schedtask.hint_holidays": {
|
||||
"en": "Runs landing on a public holiday of the chosen country are pushed to the next allowed day.",
|
||||
"ja": "選択した国の祝日に当たる回は翌営業日に繰り越し。",
|
||||
"vi": "Lịch rơi vào ngày lễ của quốc gia đã chọn sẽ tự dời sang ngày hợp lệ kế."},
|
||||
"schedtask.hint_country": {
|
||||
"en": "ISO country code for the holiday calendar (VN, JP, US… — type any code).",
|
||||
"ja": "祝日カレンダーの国コード(VN, JP, US…)。", "vi": "Mã quốc gia cho lịch nghỉ lễ (VN, JP, US… — gõ được mã bất kỳ)."},
|
||||
"schedtask.hint_flow_template": {
|
||||
"en": "Import the stages of a saved Flow template as steps here.",
|
||||
"ja": "保存済みFlowテンプレートをステップとして取り込み。",
|
||||
"vi": "Nhập các stage của Flow template đã lưu thành các bước ở đây."},
|
||||
"schedtask.hint_input_mode": {
|
||||
"en": "What the agent receives besides the description: nothing, typed text, file contents, or the output of earlier tasks.",
|
||||
"ja": "説明に加えてエージェントへ渡す入力。", "vi": "Agent nhận gì ngoài mô tả: trống, văn bản gõ tay, nội dung tệp, hoặc output các task trước."},
|
||||
"schedtask.hint_prev_task": {
|
||||
"en": "Single explicit source task for 'previous task output' (leave (none) to use all waited-for tasks).",
|
||||
"ja": "「前タスクの出力」の明示的なソース。", "vi": "Task nguồn cụ thể cho 'output task trước' (để (không) sẽ dùng tất cả task đang chờ)."},
|
||||
"schedtask.hint_output_mode": {
|
||||
"en": "Expected output format — informational for now, files always land in the artifact folder.",
|
||||
"ja": "想定する出力形式(参考情報)。", "vi": "Định dạng output mong muốn — hiện mang tính thông tin, file luôn nằm trong thư mục artifact."},
|
||||
"schedtask.hint_next_task": {
|
||||
"en": "Task to trigger after this one finishes (chain).",
|
||||
"ja": "このタスク完了後に起動するタスク(チェーン)。", "vi": "Task được kích hoạt sau khi task này xong (chuỗi)."},
|
||||
"schedtask.hint_run_next": {
|
||||
"en": "When the next task fires: on success / always / only after you confirm.",
|
||||
"ja": "次タスクの起動条件: 成功時/常に/手動確認後。", "vi": "Khi nào task sau chạy: khi thành công / luôn / chờ bạn xác nhận."},
|
||||
"schedtask.hint_pass_output": {
|
||||
"en": "This task's output.md becomes the next task's input automatically.",
|
||||
"ja": "このタスクのoutput.mdを次タスクの入力に自動投入。",
|
||||
"vi": "output.md của task này tự thành input của task sau."},
|
||||
"schedtask.hint_depends": {
|
||||
"en": "Fan-in: this task waits until ALL ticked tasks are Done, then runs automatically with their outputs available.",
|
||||
"ja": "ファンイン: チェックした全タスクがDoneになるまで待機し、自動実行。",
|
||||
"vi": "Fan-in: task này đợi TẤT CẢ task được tick Done rồi mới tự chạy, kèm output của chúng."},
|
||||
"schedtask.hint_retry": {
|
||||
"en": "Auto-retry this many times when a run fails.", "ja": "失敗時の自動リトライ回数。",
|
||||
"vi": "Tự thử lại bấy nhiêu lần khi chạy lỗi."},
|
||||
"schedtask.hint_timeout": {
|
||||
"en": "Hard limit per run (Script tasks).", "ja": "1回あたりの上限時間(Script)。",
|
||||
"vi": "Giới hạn thời gian mỗi lần chạy (task Script)."},
|
||||
"schedtask.hint_approval": {
|
||||
"en": "Safety: the scheduler will NEVER auto-run this — it parks in Waiting Input until you right-click → Run now.",
|
||||
"ja": "安全: 自動実行されず、Run nowまで待機します。",
|
||||
"vi": "An toàn: scheduler KHÔNG BAO GIỜ tự chạy task này — nó nằm ở Waiting Input tới khi bạn chuột phải → Chạy ngay."},
|
||||
"schedtask.g_input": {"en": "Input", "ja": "入力", "vi": "Input"},
|
||||
"schedtask.f_input_mode": {"en": "Input mode", "ja": "入力モード", "vi": "Chế độ input"},
|
||||
"schedtask.inmode.empty": {"en": "Empty (default)", "ja": "空(既定)", "vi": "Trống (mặc định)"},
|
||||
"schedtask.inmode.manual": {"en": "Manual text", "ja": "手入力テキスト", "vi": "Văn bản nhập tay"},
|
||||
"schedtask.inmode.file": {"en": "File(s)", "ja": "ファイル", "vi": "Tệp"},
|
||||
"schedtask.inmode.previous_task_output": {
|
||||
"en": "Previous task output", "ja": "前タスクの出力", "vi": "Output của task trước"},
|
||||
"schedtask.f_manual_text": {"en": "Prompt", "ja": "プロンプト", "vi": "Prompt"},
|
||||
"schedtask.gen_input_tooltip": {
|
||||
"en": "AI-draft the prompt from the title/description", "ja": "タイトル/説明からプロンプトをAI生成",
|
||||
"vi": "AI soạn prompt từ tiêu đề/mô tả"},
|
||||
"schedtask.f_files": {"en": "Attach files", "ja": "添付ファイル", "vi": "Đính kèm tệp"},
|
||||
"schedtask.f_links": {"en": "Attach links", "ja": "添付リンク", "vi": "Đính kèm link"},
|
||||
"schedtask.files_placeholder": {
|
||||
"en": "Local file paths, separated by ;", "ja": "ローカルファイルパス(;区切り)",
|
||||
"vi": "Đường dẫn tệp local, cách nhau bằng ;"},
|
||||
"schedtask.links_placeholder": {
|
||||
"en": "https://… URLs separated by ;", "ja": "https://… URL(;区切り)",
|
||||
"vi": "https://… các link, cách nhau bằng ;"},
|
||||
"schedtask.pick_files": {"en": "Browse…", "ja": "参照…", "vi": "Chọn tệp…"},
|
||||
"schedtask.add_link_title": {"en": "Add link", "ja": "リンクを追加", "vi": "Thêm link"},
|
||||
"schedtask.add_link_label": {"en": "URL:", "ja": "URL:", "vi": "URL:"},
|
||||
"schedtask.hint_files": {
|
||||
"en": "Attached files are always read and given to the agent as context, regardless of Input mode.",
|
||||
"ja": "添付ファイルはInputモードに関係なく常にエージェントへ渡されます。",
|
||||
"vi": "Tệp đính kèm luôn được đọc và đưa vào ngữ cảnh cho agent, bất kể chế độ Input."},
|
||||
"schedtask.hint_links": {
|
||||
"en": "Each URL is fetched (best-effort) and its text content given to the agent as context.",
|
||||
"ja": "各URLを取得し(ベストエフォート)、テキストをコンテキストとして渡します。",
|
||||
"vi": "Mỗi link được tải nội dung (khi có thể) và đưa vào ngữ cảnh cho agent."},
|
||||
"schedtask.f_prev_task": {"en": "Previous task", "ja": "前タスク", "vi": "Task trước"},
|
||||
"schedtask.g_output": {"en": "Output", "ja": "出力", "vi": "Output"},
|
||||
"schedtask.f_output_mode": {"en": "Output mode", "ja": "出力モード", "vi": "Chế độ output"},
|
||||
"schedtask.g_dependency": {"en": "Dependency / Next task", "ja": "依存 / 次タスク", "vi": "Phụ thuộc / Task tiếp theo"},
|
||||
"schedtask.f_next_task": {"en": "Next task", "ja": "次タスク", "vi": "Task tiếp theo"},
|
||||
"schedtask.f_run_next": {"en": "Run next task", "ja": "次タスクの実行", "vi": "Chạy task tiếp theo"},
|
||||
"schedtask.runnext.none": {"en": "Don't run next task", "ja": "実行しない", "vi": "Không chạy task sau"},
|
||||
"schedtask.runnext.run_after_success": {
|
||||
"en": "Run after success", "ja": "成功後に実行", "vi": "Chạy khi task này thành công"},
|
||||
"schedtask.runnext.run_always": {"en": "Always run", "ja": "常に実行", "vi": "Luôn chạy (kể cả lỗi)"},
|
||||
"schedtask.runnext.run_after_manual_confirm": {
|
||||
"en": "Wait for my confirmation", "ja": "手動確認後に実行", "vi": "Chờ tôi xác nhận rồi chạy"},
|
||||
"schedtask.pass_output": {
|
||||
"en": "Use this task's output as next task's input",
|
||||
"ja": "このタスクの出力を次タスクの入力にする",
|
||||
"vi": "Dùng output task này làm input task sau"},
|
||||
"schedtask.next_paused_warn": {
|
||||
"en": "The selected next task is paused — it will be skipped when this task finishes.",
|
||||
"ja": "選択した次タスクは一時停止中のため、完了時にスキップされます。",
|
||||
"vi": "Task tiếp theo đang tạm dừng — sẽ bị bỏ qua khi task này chạy xong."},
|
||||
"schedtask.none": {"en": "(none)", "ja": "(なし)", "vi": "(không)"},
|
||||
"schedtask.g_execution": {"en": "Execution", "ja": "実行設定", "vi": "Thực thi"},
|
||||
"schedtask.f_retry": {"en": "Max retry", "ja": "最大リトライ", "vi": "Số lần thử lại"},
|
||||
"schedtask.f_timeout": {"en": "Timeout", "ja": "タイムアウト", "vi": "Thời gian tối đa"},
|
||||
"schedtask.requires_approval": {
|
||||
"en": "Requires approval (scheduler will NOT auto-run; waits for Run now)",
|
||||
"ja": "承認必須(自動実行されず、手動のRun nowを待ちます)",
|
||||
"vi": "Cần phê duyệt (không tự chạy theo lịch; chờ bấm Chạy ngay)"},
|
||||
"schedtask.notify_ok": {"en": "Notify Teams on complete", "ja": "完了時にTeams通知", "vi": "Báo Teams khi xong"},
|
||||
"schedtask.notify_err": {"en": "Notify Teams on error", "ja": "エラー時にTeams通知", "vi": "Báo Teams khi lỗi"},
|
||||
"schedtask.title_required": {"en": "Please enter a title.", "ja": "タイトルを入力してください。", "vi": "Vui lòng nhập tiêu đề."},
|
||||
# AI create dialog
|
||||
"schedtask.ai_desc_label": {
|
||||
"en": "Describe what you want to automate:", "ja": "自動化したい内容を記述:",
|
||||
"vi": "Mô tả việc bạn muốn tự động hoá:"},
|
||||
"schedtask.ai_desc_ph": {
|
||||
"en": "e.g. Every Monday 9:00, use Code to read new CAE data and build a markdown report, then have Cowork draft a team email from it.",
|
||||
"ja": "例: 毎週月曜9時、CodeでCAEデータを読み込みレポート作成、その後Coworkでメール下書きを作成。",
|
||||
"vi": "vd: Mỗi thứ 2 lúc 9h, dùng Code đọc dữ liệu CAE mới tạo báo cáo markdown, sau đó Cowork soạn email draft gửi team."},
|
||||
"schedtask.ai_generate": {"en": "Generate plan", "ja": "プランを生成", "vi": "Tạo kế hoạch"},
|
||||
"schedtask.ai_generating": {"en": "Generating…", "ja": "生成中…", "vi": "Đang tạo…"},
|
||||
"schedtask.ai_preview_label": {
|
||||
"en": "Preview (nothing is created until you confirm):",
|
||||
"ja": "プレビュー(確認するまで作成されません):",
|
||||
"vi": "Xem trước (chưa tạo gì cho tới khi bạn xác nhận):"},
|
||||
"schedtask.ai_confirm": {"en": "Create tasks", "ja": "タスクを作成", "vi": "Tạo các task"},
|
||||
"schedtask.tab_ai": {"en": "AI gen task", "ja": "AIタスク生成", "vi": "AI gen task"},
|
||||
"schedtask.tab_import": {"en": "Import", "ja": "インポート", "vi": "Import"},
|
||||
"schedtask.export_template_btn": {
|
||||
"en": "Create Excel template…", "ja": "Excelテンプレートを作成…",
|
||||
"vi": "Tạo template Excel…"},
|
||||
"schedtask.import_pick_btn": {"en": "Choose file…", "ja": "ファイルを選択…", "vi": "Chọn file…"},
|
||||
"schedtask.drop_hint": {
|
||||
"en": "…or drag & drop the filled .xlsx here",
|
||||
"ja": "…または記入済みの .xlsx をここにドラッグ&ドロップ",
|
||||
"vi": "…hoặc kéo-thả file .xlsx đã điền vào đây"},
|
||||
"schedtask.f_depends_on": {
|
||||
"en": "Wait for tasks (all must be Done)", "ja": "待機するタスク(全てDone必須)",
|
||||
"vi": "Chờ các task (tất cả phải Done)"},
|
||||
"schedtask.gen_desc_tooltip": {
|
||||
"en": "Generate the Prompt from this description (the title is not used)",
|
||||
"ja": "この説明からプロンプトを生成(タイトルは使用しません)",
|
||||
"vi": "Sinh Prompt từ mô tả này (không dùng tiêu đề)"},
|
||||
"schedtask.gen_needs_description": {
|
||||
"en": "Enter a description first — the Prompt is generated from it.",
|
||||
"ja": "先に説明を入力してください。プロンプトは説明から生成されます。",
|
||||
"vi": "Hãy nhập mô tả trước — Prompt được sinh ra từ mô tả."},
|
||||
# ---- dashboard_tab.py ------------------------------------------------
|
||||
"dashboard.title": {"en": "Dashboard — token usage & cost", "ja": "Dashboard — トークン使用量とコスト",
|
||||
"vi": "Dashboard — token & chi phí"},
|
||||
"dashboard.period.today": {"en": "Today", "ja": "今日", "vi": "Hôm nay"},
|
||||
"dashboard.period.week": {"en": "Last 7 days", "ja": "過去7日", "vi": "7 ngày qua"},
|
||||
"dashboard.period.month": {"en": "Last 30 days", "ja": "過去30日", "vi": "30 ngày qua"},
|
||||
"dashboard.period.all": {"en": "All time", "ja": "全期間", "vi": "Toàn bộ"},
|
||||
"dashboard.source_all": {"en": "All tasks/sessions", "ja": "全タスク/セッション", "vi": "Mọi task/phiên"},
|
||||
"dashboard.refresh_tooltip": {"en": "Refresh now", "ja": "今すぐ更新", "vi": "Làm mới ngay"},
|
||||
"dashboard.card_total": {"en": "Total tokens", "ja": "合計トークン", "vi": "Tổng token"},
|
||||
"dashboard.card_in": {"en": "Input", "ja": "入力", "vi": "Input"},
|
||||
"dashboard.card_out": {"en": "Output", "ja": "出力", "vi": "Output"},
|
||||
"dashboard.card_cache": {"en": "Cache", "ja": "キャッシュ", "vi": "Cache"},
|
||||
"dashboard.card_cost": {"en": "Cost", "ja": "コスト", "vi": "Chi phí"},
|
||||
"dashboard.card_turns": {"en": "{n} turns", "ja": "{n} ターン", "vi": "{n} lượt"},
|
||||
"dashboard.prices_label": {
|
||||
"en": "Unit price (USD / 1M tokens):", "ja": "単価 (USD / 100万トークン):",
|
||||
"vi": "Đơn giá (USD / 1 triệu token):"},
|
||||
"dashboard.price_in": {"en": "In", "ja": "入力", "vi": "In"},
|
||||
"dashboard.price_out": {"en": "Out", "ja": "出力", "vi": "Out"},
|
||||
"dashboard.price_cache": {"en": "Cache", "ja": "キャッシュ", "vi": "Cache"},
|
||||
"dashboard.habits_title": {
|
||||
"en": "Usage habits overview", "ja": "利用傾向の概要", "vi": "Tổng quan thói quen sử dụng"},
|
||||
"dashboard.chart_title": {"en": "Tokens / cost over time", "ja": "トークン/コスト推移",
|
||||
"vi": "Token / chi phí theo thời gian"},
|
||||
"dashboard.strategy_btn": {"en": "Apply saving strategy", "ja": "節約戦略を適用",
|
||||
"vi": "Áp dụng chiến lược tiết kiệm"},
|
||||
"dashboard.strategy_tooltip": {
|
||||
"en": "Apply the AI's cost-saving strategy: auto-compress earlier + digest context before each turn.",
|
||||
"ja": "AIの節約戦略を適用:早めに自動圧縮+各ターン前にコンテキストを要約。",
|
||||
"vi": "Áp dụng chiến lược tiết kiệm của AI: tự động nén sớm hơn + tóm gọn ngữ cảnh trước mỗi lượt."},
|
||||
"dashboard.strategy_title": {"en": "Apply saving strategy", "ja": "節約戦略の適用",
|
||||
"vi": "Áp dụng chiến lược tiết kiệm"},
|
||||
"dashboard.strategy_confirm": {
|
||||
"en": "Turn on auto-compress (earlier, at 60%) and compress context before each turn to cut tokens?",
|
||||
"ja": "自動圧縮(60%で早めに)とターン前のコンテキスト圧縮を有効にしてトークンを削減しますか?",
|
||||
"vi": "Bật tự động nén (sớm hơn, ở 60%) và nén ngữ cảnh trước mỗi lượt để giảm token?"},
|
||||
"dashboard.strategy_applied": {
|
||||
"en": "Saving strategy applied: auto-compress on, compress-before-send on.",
|
||||
"ja": "節約戦略を適用:自動圧縮ON、送信前圧縮ON。",
|
||||
"vi": "Đã áp dụng: bật tự động nén và nén trước khi gửi."},
|
||||
"dashboard.gran_day": {"en": "By day", "ja": "日別", "vi": "Theo ngày"},
|
||||
"dashboard.gran_week": {"en": "By week", "ja": "週別", "vi": "Theo tuần"},
|
||||
"dashboard.gran_month": {"en": "By month", "ja": "月別", "vi": "Theo tháng"},
|
||||
"dashboard.gran_year": {"en": "By year", "ja": "年別", "vi": "Theo năm"},
|
||||
"dashboard.ref_last_week": {"en": "Last week", "ja": "先週", "vi": "Tuần trước"},
|
||||
"dashboard.ref_last_month": {"en": "Last month", "ja": "先月", "vi": "Tháng trước"},
|
||||
"dashboard.ref_last_year": {"en": "Last year", "ja": "昨年", "vi": "Năm trước"},
|
||||
"usage.budget_title": {"en": "Budget", "ja": "予算", "vi": "Ngân sách"},
|
||||
"usage.budget_no_budget": {"en": "No budget set", "ja": "予算未設定", "vi": "Chưa đặt Budget"},
|
||||
"usage.budget_used_pct": {"en": "{pct}% used", "ja": "{pct}% 使用済み", "vi": "Đã dùng {pct}%"},
|
||||
"usage.budget_over_warning": {"en": "⚠ Over 85% of budget used",
|
||||
"ja": "⚠ 予算の85%以上を使用",
|
||||
"vi": "⚠ Đã dùng quá 85% Budget"},
|
||||
"usage.budget_apply_tooltip": {"en": "Set this as the budget (starts a fresh remaining-balance window)",
|
||||
"ja": "この金額を予算として設定(残高の計算を今から開始)",
|
||||
"vi": "Đặt số này làm Budget (tính số dư mới từ bây giờ)"},
|
||||
"usage.budget_spin_tooltip": {"en": "Enter the budget amount directly, then click ✓",
|
||||
"ja": "予算額を直接入力して ✓ をクリック",
|
||||
"vi": "Nhập Budget trực tiếp rồi bấm ✓"},
|
||||
"dashboard.chart_prev": {"en": "Previous period", "ja": "前の期間", "vi": "Kỳ trước"},
|
||||
"dashboard.chart_next": {"en": "Next period", "ja": "次の期間", "vi": "Kỳ sau"},
|
||||
"dashboard.metric_cost": {"en": "Cost", "ja": "コスト", "vi": "Chi phí"},
|
||||
"dashboard.metric_tokens": {"en": "Tokens", "ja": "トークン", "vi": "Token"},
|
||||
"dashboard.h_top": {"en": "Top token consumers (task/session)", "ja": "トークン消費上位(タスク/セッション)",
|
||||
"vi": "Tiêu tốn token nhiều nhất (task/phiên)"},
|
||||
"dashboard.h_by_source": {"en": "By area", "ja": "領域別", "vi": "Theo khu vực"},
|
||||
"dashboard.h_avg": {"en": "Average per prompt", "ja": "1プロンプト平均", "vi": "Trung bình mỗi prompt"},
|
||||
"dashboard.h_busiest_day": {"en": "Busiest day", "ja": "最も使った日", "vi": "Ngày dùng nhiều nhất"},
|
||||
"dashboard.h_busiest_hour": {"en": "Busiest hour", "ja": "最も使う時間帯", "vi": "Khung giờ hay dùng"},
|
||||
"dashboard.no_data": {
|
||||
"en": "No usage recorded in this period yet — run a chat or a task first.",
|
||||
"ja": "この期間の使用記録はまだありません。チャットやタスクを実行してください。",
|
||||
"vi": "Chưa có dữ liệu sử dụng trong giai đoạn này — hãy chạy chat hoặc task trước."},
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
"""Chuỗi hiển thị — phần libreoffice_view.
|
||||
|
||||
Cắt ra từ ``i18n.py``: một dict 3.000 dòng không mở nổi để sửa một chữ.
|
||||
Cắt theo mốc phân đoạn có sẵn, nên mỗi cụm vẫn là các màn đi liền nhau.
|
||||
``i18n.py`` gộp tất cả lại thành ``STRINGS``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict
|
||||
|
||||
STRINGS: Dict[str, Dict[str, str]] = {
|
||||
"structure.export_title": {"en": "Export graph PNG", "ja": "グラフを PNG でエクスポート", "vi": "Xuất đồ thị ra PNG"},
|
||||
"structure.export_done": {"en": "Graph exported to {path}", "ja": "グラフを {path} にエクスポートしました", "vi": "Đã xuất đồ thị ra {path}"},
|
||||
"structure.export_failed": {"en": "Export failed: {err}", "ja": "エクスポート失敗: {err}", "vi": "Xuất thất bại: {err}"},
|
||||
"structure.scan_first": {"en": "Scan a graph first.", "ja": "先にグラフをスキャンしてください。", "vi": "Hãy Scan đồ thị trước."},
|
||||
"structure.related_sources": {
|
||||
"en": "Related files (click to open):",
|
||||
"ja": "関連ファイル(クリックで開く):",
|
||||
"vi": "Tệp liên quan (bấm để mở):"},
|
||||
"structure.legend.dir": {"en": "Folder", "ja": "フォルダ", "vi": "Thư mục"},
|
||||
"structure.legend.file": {"en": "File", "ja": "ファイル", "vi": "Tệp"},
|
||||
"structure.legend.class": {"en": "Class", "ja": "クラス", "vi": "Class"},
|
||||
"structure.legend.function": {"en": "Function", "ja": "関数", "vi": "Function"},
|
||||
"structure.legend.method": {"en": "Method", "ja": "メソッド", "vi": "Method"},
|
||||
"structure.legend.module": {"en": "Module", "ja": "モジュール", "vi": "Module"},
|
||||
"structure.legend.section": {"en": "Section", "ja": "セクション", "vi": "Mục"},
|
||||
"structure.legend.json_key": {"en": "JSON key", "ja": "JSONキー", "vi": "Khóa JSON"},
|
||||
"structure.legend.entities": {"en": "Entities", "ja": "エンティティ", "vi": "Thực thể"},
|
||||
"structure.legend.relationships": {"en": "Relationships", "ja": "関係", "vi": "Quan hệ"},
|
||||
"structure.show_label": {"en": "Show label", "ja": "ラベル表示", "vi": "Hiện nhãn"},
|
||||
"structure.show_relationship": {
|
||||
"en": "Show relationship", "ja": "関係を表示", "vi": "Hiện quan hệ"},
|
||||
"structure.edge.contains": {"en": "contains", "ja": "含む", "vi": "chứa"},
|
||||
"structure.edge.defines": {"en": "defines", "ja": "定義", "vi": "định nghĩa"},
|
||||
"structure.edge.method": {"en": "method", "ja": "メソッド", "vi": "phương thức"},
|
||||
"structure.edge.imports": {"en": "imports", "ja": "インポート", "vi": "import"},
|
||||
"structure.edge.subsection": {"en": "subsection", "ja": "サブセクション", "vi": "mục con"},
|
||||
|
||||
# ---- libreoffice_view.py -------------------------------------------
|
||||
"libreoffice.open_btn": {"en": "Open in LibreOffice", "ja": "LibreOffice で開く", "vi": "Mở bằng LibreOffice"},
|
||||
"libreoffice.not_found": {
|
||||
"en": ("LibreOffice was not found. Install LibreOffice (or set the "
|
||||
"SOFFICE_PATH environment variable) to view and edit documents here."),
|
||||
"ja": "LibreOffice が見つかりません。ここで文書を表示/編集するには LibreOffice をインストールするか、環境変数 SOFFICE_PATH を設定してください。",
|
||||
"vi": "Không tìm thấy LibreOffice. Hãy cài LibreOffice (hoặc đặt biến môi trường SOFFICE_PATH) để xem/sửa tài liệu tại đây."},
|
||||
"libreoffice.windows_only": {
|
||||
"en": "Embedding the editor is available on Windows. Click below to open this document in LibreOffice.",
|
||||
"ja": "エディタの埋め込みは Windows でのみ利用可能です。下のボタンで LibreOffice で開いてください。",
|
||||
"vi": "Nhúng trình soạn thảo chỉ khả dụng trên Windows. Bấm bên dưới để mở tài liệu bằng LibreOffice."},
|
||||
"libreoffice.start_failed": {"en": "Could not start LibreOffice ({err}).", "ja": "LibreOffice を起動できませんでした({err})。", "vi": "Không khởi động được LibreOffice ({err})."},
|
||||
"libreoffice.opening": {"en": "Opening the document in LibreOffice…", "ja": "LibreOffice で文書を開いています…", "vi": "Đang mở tài liệu bằng LibreOffice…"},
|
||||
"libreoffice.embed_failed": {
|
||||
"en": "Couldn't embed the LibreOffice window. You can open it in a separate window instead.",
|
||||
"ja": "LibreOffice ウィンドウを埋め込めませんでした。別ウィンドウで開くことができます。",
|
||||
"vi": "Không nhúng được cửa sổ LibreOffice. Bạn có thể mở nó ở cửa sổ riêng."},
|
||||
"libreoffice.embed_error": {"en": "Couldn't embed LibreOffice ({err}).", "ja": "LibreOffice を埋め込めませんでした({err})。", "vi": "Không nhúng được LibreOffice ({err})."},
|
||||
|
||||
# ---- monitoring_tab.py (📊 Monitoring Dashboard) --------------------
|
||||
"monitoring.title": {"en": "Monitoring Dashboard", "ja": "モニタリングダッシュボード", "vi": "Bảng giám sát"},
|
||||
"monitoring.refresh": {"en": "Refresh", "ja": "更新", "vi": "Làm mới"},
|
||||
"monitoring.tab_security": {"en": "Security", "ja": "セキュリティ", "vi": "Bảo mật"},
|
||||
"monitoring.tab_mcp": {"en": "MCP", "ja": "MCP", "vi": "MCP"},
|
||||
"monitoring.tab_actions": {"en": "Actions", "ja": "アクション", "vi": "Hành động"},
|
||||
"monitoring.tab_agents": {"en": "Agent", "ja": "エージェント", "vi": "Agent"},
|
||||
"monitoring.tab_accounts": {"en": "Accounts", "ja": "アカウント", "vi": "Tài khoản"},
|
||||
"monitoring.col_time": {"en": "Time", "ja": "時刻", "vi": "Thời gian"},
|
||||
"monitoring.col_role": {"en": "Agent Role", "ja": "エージェント役割", "vi": "Vai trò Agent"},
|
||||
"monitoring.col_name": {"en": "Action", "ja": "アクション", "vi": "Hành động"},
|
||||
"monitoring.col_result": {"en": "Result", "ja": "結果", "vi": "Kết quả"},
|
||||
# Security Events shows WHICH rule fired instead of a result that is always
|
||||
# the same — every security_block is recorded with ok=False.
|
||||
"monitoring.col_action": {"en": "Action", "ja": "アクション", "vi": "Hành động"},
|
||||
# The fourth KPI tile on Overview, as the wireframe labels it.
|
||||
"monitoring.overview_calls": {"en": "Calls", "ja": "呼び出し", "vi": "Lượt gọi"},
|
||||
# The fold under the Sandbox summary line — the wireframe shows only the
|
||||
# summary, so the ID / created / uptime / limits rows live behind this.
|
||||
"monitoring.overview_disk_free": {
|
||||
"en": "{size} free", "ja": "空き {size}", "vi": "{size} trống"},
|
||||
"monitoring.overview_disk_label": {"en": "Disk", "ja": "ディスク", "vi": "Đĩa"},
|
||||
"monitoring.overview_sbx_detail": {
|
||||
"en": "Details", "ja": "詳細", "vi": "Chi tiết"},
|
||||
"monitoring.col_detail": {"en": "Detail", "ja": "詳細", "vi": "Chi tiết"},
|
||||
"monitoring.col_account": {"en": "Account", "ja": "アカウント", "vi": "Tài khoản"},
|
||||
"monitoring.col_machine": {"en": "Machine", "ja": "マシン", "vi": "Máy"},
|
||||
"monitoring.col_agent": {"en": "Agent", "ja": "エージェント", "vi": "Agent"},
|
||||
"monitoring.col_source": {"en": "Source", "ja": "ソース", "vi": "Nguồn"},
|
||||
"monitoring.active_n": {"en": "{n} running", "ja": "{n} 件実行中", "vi": "{n} đang chạy"},
|
||||
"monitoring.idle": {"en": "Idle", "ja": "アイドル", "vi": "Rảnh"},
|
||||
"monitoring.agent_status_title": {
|
||||
"en": "Agent Status", "ja": "エージェント状態", "vi": "Trạng thái Agent"},
|
||||
"monitoring.source_cowork": {
|
||||
"en": "Cowork tab's active turns", "ja": "Cowork タブの実行中ターン",
|
||||
"vi": "Lượt đang chạy của tab Cowork"},
|
||||
"monitoring.source_task": {
|
||||
"en": "Schedule Task's running tasks", "ja": "Schedule Task の実行中タスク",
|
||||
"vi": "Task đang chạy trong Schedule Task"},
|
||||
"monitoring.source_knowledge": {
|
||||
"en": "GraphRAG's Ask box", "ja": "GraphRAG の Ask ボックス", "vi": "Ô hỏi của GraphRAG"},
|
||||
"monitoring.source_code": {
|
||||
"en": "Runs inside a Task Agent run when the task type is Code",
|
||||
"ja": "タスクタイプが Code の場合、Task Agent の実行内で動作します",
|
||||
"vi": "Chạy bên trong một lượt Task Agent khi loại task là Code"},
|
||||
"monitoring.source_planner": {
|
||||
"en": "A phase inside a running Cowork/Task turn (update_plan) — not tracked separately",
|
||||
"ja": "実行中の Cowork/Task ターン内の一段階(update_plan)— 個別には追跡されません",
|
||||
"vi": "Một giai đoạn trong lượt Cowork/Task đang chạy (update_plan) — không theo dõi riêng"},
|
||||
"monitoring.source_reasoning": {
|
||||
"en": "The model's streamed reasoning within a running turn — not tracked separately",
|
||||
"ja": "実行中のターン内でモデルがストリーミングする推論 — 個別には追跡されません",
|
||||
"vi": "Luồng suy luận (reasoning) của model trong lượt đang chạy — không theo dõi riêng"},
|
||||
"monitoring.source_security": {
|
||||
"en": "Agent Security's prompt/attachment/command validation — runs inline on the active turn",
|
||||
"ja": "エージェントセキュリティのプロンプト/添付/コマンド検証 — 実行中のターン内でインライン実行",
|
||||
"vi": "Kiểm duyệt prompt/đính kèm/lệnh của Agent Security — chạy inline trong lượt đang chạy"},
|
||||
"monitoring.on": {"en": "On", "ja": "オン", "vi": "Bật"},
|
||||
"monitoring.off": {"en": "Off", "ja": "オフ", "vi": "Tắt"},
|
||||
|
||||
# ---- monitoring_tab.py — Overview card dashboard ---------------------
|
||||
"monitoring.tab_overview": {"en": "Overview", "ja": "概要", "vi": "Tổng quan"},
|
||||
"monitoring.overview_usage_title": {
|
||||
"en": "Token & Cost", "ja": "トークンとコスト", "vi": "Token & Chi phí"},
|
||||
"monitoring.overview_currency": {"en": "Currency:", "ja": "通貨:", "vi": "Tiền tệ:"},
|
||||
"monitoring.tab_agents_admin": {
|
||||
"en": "Agents Admin", "ja": "エージェント管理", "vi": "Agents Admin"},
|
||||
"monitoring.tab_tools": {"en": "Tools", "ja": "ツール", "vi": "Công cụ"},
|
||||
|
||||
# ---- tools_admin_tab.py — govern built-in tools + Connectors/MCP -------
|
||||
"tools_admin.hint": {
|
||||
"en": "Enable or disable the built-in agent tools below. A tool toggled off is removed "
|
||||
"from the agent's toolset. MCP / REST-API connectors are set up in the Connector "
|
||||
"sub-tab.",
|
||||
"ja": "下の組み込みエージェントツールをオン/オフします。オフにしたツールはツールセットから除外"
|
||||
"されます。MCP / REST-APIコネクターは「Connector」サブタブで設定します。",
|
||||
"vi": "Bật/tắt các tool tích hợp bên dưới. Tool bị tắt sẽ bị loại khỏi bộ công cụ của agent. "
|
||||
"Connector MCP / REST-API được thiết lập ở tab con Connector."},
|
||||
"tools_admin.refresh": {"en": "Refresh", "ja": "更新", "vi": "Làm mới"},
|
||||
"tools_admin.url_fetch_group": {
|
||||
"en": "Web access (fetch_url)", "ja": "Webアクセス (fetch_url)",
|
||||
"vi": "Truy cập web (fetch_url)"},
|
||||
"tools_admin.internet_disabled": {
|
||||
"en": "Web access is OFF — enable the fetch_url tool above to allow internet access.",
|
||||
"ja": "Web アクセスはオフです — 上の fetch_url ツールを有効にするとインターネットに接続できます。",
|
||||
"vi": "Truy cập web đang TẮT — bật tool fetch_url ở trên để cho phép truy cập internet."},
|
||||
"tools_admin.subtab_tool": {"en": "Tool", "ja": "ツール", "vi": "Tool"},
|
||||
"tools_admin.subtab_connector": {"en": "Connector", "ja": "コネクター", "vi": "Connector"},
|
||||
"monitoring.tab_icons": {"en": "Icons", "ja": "アイコン", "vi": "Icon"},
|
||||
"icons_admin.title": {"en": "Icons", "ja": "アイコン", "vi": "Icon"},
|
||||
"icons_admin.hint": {
|
||||
"en": "Icons you can use for agents and flows. Type a name into a step/agent's Icon field to "
|
||||
"use it. Add your own SVG icons below — they become usable by name immediately.",
|
||||
"ja": "エージェントやフローに使えるアイコン。ステップ/エージェントのアイコン欄に名前を入力すると使えます。"
|
||||
"下から独自のSVGアイコンを追加でき、名前ですぐ使えます。",
|
||||
"vi": "Các icon dùng cho agent và flow. Gõ tên vào ô Icon của step/agent để dùng. Thêm icon SVG "
|
||||
"của bạn ở dưới — dùng được ngay bằng tên."},
|
||||
"icons_admin.search": {"en": "Search icons by name…", "ja": "名前でアイコンを検索…", "vi": "Tìm icon theo tên…"},
|
||||
"icons_admin.builtin": {"en": "Built-in icons", "ja": "組込みアイコン", "vi": "Icon tích hợp"},
|
||||
"icons_admin.custom": {"en": "Custom icons", "ja": "カスタムアイコン", "vi": "Icon tùy chỉnh"},
|
||||
"icons_admin.add": {"en": "Add SVG file", "ja": "SVGファイルを追加", "vi": "Thêm tệp SVG"},
|
||||
"icons_admin.paste": {"en": "Paste SVG", "ja": "SVGを貼付", "vi": "Dán SVG"},
|
||||
"icons_admin.paste_prompt": {"en": "Paste the SVG markup:", "ja": "SVGマークアップを貼り付け:",
|
||||
"vi": "Dán mã SVG:"},
|
||||
"icons_admin.delete": {"en": "Delete custom", "ja": "カスタムを削除", "vi": "Xóa tùy chỉnh"},
|
||||
"icons_admin.name_prompt": {"en": "Icon name (used in the Icon field)", "ja": "アイコン名(アイコン欄で使用)",
|
||||
"vi": "Tên icon (dùng ở ô Icon)"},
|
||||
"icons_admin.select_custom": {"en": "Select a custom icon to delete.",
|
||||
"ja": "削除するカスタムアイコンを選択してください。",
|
||||
"vi": "Hãy chọn một icon tùy chỉnh để xóa."},
|
||||
"tools_admin.jira_note": {
|
||||
"en": "Jira connection setup moved to the Connector tab → set it up there; here you only turn "
|
||||
"the jira_search / jira_get_issue tools on or off.",
|
||||
"ja": "Jira接続の設定はConnectorタブに移動しました。設定はそちらで。ここでは jira_search / "
|
||||
"jira_get_issue ツールの有効/無効のみ切り替えます。",
|
||||
"vi": "Phần thiết lập kết nối Jira đã chuyển sang tab Connector → cài đặt ở đó; ở đây chỉ bật/tắt "
|
||||
"tool jira_search / jira_get_issue."},
|
||||
"connectors.jira_group": {"en": "Jira (read)", "ja": "Jira(読み取り)", "vi": "Jira (đọc)"},
|
||||
"connectors.jira_hint": {
|
||||
"en": "Connect once, then just paste a Jira link into Cowork or a Co4E step — the agent reads it "
|
||||
"automatically (no issue key needed). Read-only. A public Jira link works with no setup; "
|
||||
"a private one needs this connection. Create a token: id.atlassian.com → Security → API tokens.",
|
||||
"ja": "一度接続すれば、Cowork や Co4E ステップに Jira リンクを貼るだけで自動で読み取ります(課題キー不要)。"
|
||||
"読み取り専用。公開リンクは設定不要、非公開はこの接続が必要。トークン作成: id.atlassian.com → セキュリティ → APIトークン。",
|
||||
"vi": "Kết nối một lần, rồi chỉ cần dán link Jira vào Cowork hoặc bước Co4E — agent tự đọc (không cần "
|
||||
"issue key). Chỉ đọc. Link Jira công khai không cần cài đặt; link riêng tư cần kết nối này. "
|
||||
"Tạo token: id.atlassian.com → Security → API tokens."},
|
||||
"connectors.jira_paste": {"en": "Paste a link", "ja": "リンクを貼付", "vi": "Dán link"},
|
||||
"connectors.jira_paste_placeholder": {
|
||||
"en": "Paste any Jira link — fills the base URL for you",
|
||||
"ja": "Jiraのリンクを貼ると、ベースURLが自動入力されます",
|
||||
"vi": "Dán bất kỳ link Jira nào — tự điền Base URL"},
|
||||
"connectors.jira_url": {"en": "Base URL", "ja": "ベースURL", "vi": "Base URL"},
|
||||
"connectors.jira_email": {"en": "Email", "ja": "メール", "vi": "Email"},
|
||||
"connectors.jira_token": {"en": "API token", "ja": "APIトークン", "vi": "API token"},
|
||||
"connectors.jira_save": {"en": "Save", "ja": "保存", "vi": "Lưu"},
|
||||
"connectors.jira_test": {"en": "Test connection", "ja": "接続テスト", "vi": "Kiểm tra kết nối"},
|
||||
"connectors.jira_saved": {"en": "Saved.", "ja": "保存しました。", "vi": "Đã lưu."},
|
||||
"connectors.jira_connected": {"en": "connected", "ja": "接続済み", "vi": "đã kết nối"},
|
||||
"connectors.jira_not_set": {"en": "not configured", "ja": "未設定", "vi": "chưa cấu hình"},
|
||||
"connectors.jira_setup_hint": {
|
||||
"en": "Double-click to connect Jira (paste any Jira link — no per-request setup after that).",
|
||||
"ja": "ダブルクリックで Jira に接続(Jira リンクを貼るだけ、以降は設定不要)。",
|
||||
"vi": "Nhấp đúp để kết nối Jira (dán bất kỳ link Jira nào — sau đó không cần thiết lập gì thêm)."},
|
||||
"connectors.builtin_auto": {
|
||||
"en": "Built-in, connects automatically", "ja": "組み込み、自動接続",
|
||||
"vi": "Tích hợp, tự kết nối"},
|
||||
"connectors.connect_external": {
|
||||
"en": "Connect to external connectors",
|
||||
"ja": "外部コネクタに接続する",
|
||||
"vi": "Kết nối tới connector bên ngoài"},
|
||||
"connectors.connect_external_tooltip": {
|
||||
"en": ("Master switch (default ON): when off, the agent connects to NO external "
|
||||
"connector or MCP server — the per-connector settings below are ignored."),
|
||||
"ja": "マスタースイッチ(既定オン): オフにすると、エージェントは外部コネクタ/MCPサーバーに"
|
||||
"一切接続しません(下の個別設定は無視されます)。",
|
||||
"vi": ("Công tắc tổng (mặc định BẬT): khi tắt, agent sẽ KHÔNG kết nối tới bất kỳ connector "
|
||||
"hay MCP server bên ngoài nào — các thiết lập từng connector bên dưới bị bỏ qua.")},
|
||||
"connectors.jira_testing": {"en": "Testing…", "ja": "テスト中…", "vi": "Đang kiểm tra…"},
|
||||
"connectors.jira_ok": {"en": "✓ Connected to Jira.", "ja": "✓ Jiraに接続できました。", "vi": "✓ Kết nối Jira thành công."},
|
||||
"connectors.jira_fail": {"en": "✗ {err}", "ja": "✗ {err}", "vi": "✗ {err}"},
|
||||
"connectors.jira_need_fields": {
|
||||
"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."},
|
||||
"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 "
|
||||
"reads it automatically (no issue key needed). Read-only. Private Jira needs this one-time "
|
||||
"connection; a public Jira link works with no setup. API token: id.atlassian.com → "
|
||||
"Security → API tokens. Turn the jira tools on/off in the list above.",
|
||||
"ja": "一度接続すれば、あとは Cowork や Co4E ステップに Jira のリンクを貼るだけで自動的に読み取ります"
|
||||
"(課題キー不要)。読み取り専用。非公開Jiraはこの一度の接続が必要、公開リンクは設定不要。"
|
||||
"APIトークン: id.atlassian.com → セキュリティ → APIトークン。ツールの有効/無効は上の一覧で。",
|
||||
"vi": "Kết nối một lần, sau đó chỉ cần dán link Jira vào Cowork hoặc bước Co4E — agent tự đọc "
|
||||
"(không cần nhập issue key). Chỉ đọc. Jira riêng tư cần kết nối một lần này; link Jira công "
|
||||
"khai thì không cần cài đặt. API token: id.atlassian.com → Security → API tokens. Bật/tắt "
|
||||
"tool jira ở danh sách phía trên."},
|
||||
"tools_admin.jira_url": {"en": "Base URL", "ja": "ベースURL", "vi": "Base URL"},
|
||||
"tools_admin.jira_email": {"en": "Email", "ja": "メール", "vi": "Email"},
|
||||
"tools_admin.jira_token": {"en": "API token", "ja": "APIトークン", "vi": "API token"},
|
||||
"tools_admin.jira_save": {"en": "Save", "ja": "保存", "vi": "Lưu"},
|
||||
"tools_admin.jira_test": {"en": "Test connection", "ja": "接続テスト", "vi": "Kiểm tra kết nối"},
|
||||
"tools_admin.jira_saved": {"en": "Saved.", "ja": "保存しました。", "vi": "Đã lưu."},
|
||||
"tools_admin.jira_testing": {"en": "Testing…", "ja": "テスト中…", "vi": "Đang kiểm tra…"},
|
||||
"tools_admin.jira_ok": {"en": "✓ Connected to Jira.", "ja": "✓ Jiraに接続できました。", "vi": "✓ Kết nối Jira thành công."},
|
||||
"tools_admin.jira_fail": {"en": "✗ {err}", "ja": "✗ {err}", "vi": "✗ {err}"},
|
||||
"tools_admin.jira_need_fields": {
|
||||
"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."},
|
||||
# ---- Co4E (node-graph workflow studio) --------------------------------
|
||||
"workspace.tab_co4e": {"en": "Co4E", "ja": "Co4E", "vi": "Co4E"},
|
||||
"workspace.tab_co4e_tooltip": {
|
||||
"en": "Co4E — Code for Everyone, Cowork for Everyone",
|
||||
"ja": "Co4E — Code for Everyone, Cowork for Everyone",
|
||||
"vi": "Co4E — Code for Everyone, Cowork for Everyone",
|
||||
},
|
||||
"co4e.untitled": {"en": "Untitled flow", "ja": "無題のフロー", "vi": "Flow chưa đặt tên"},
|
||||
"co4e.tab_workflows": {"en": "Workflows", "ja": "ワークフロー", "vi": "Workflows"},
|
||||
"co4e.tab_agents": {"en": "Agents", "ja": "エージェント", "vi": "Agents"},
|
||||
"co4e.tab_skills": {"en": "Skills", "ja": "スキル", "vi": "Skills"},
|
||||
"co4e.new": {"en": "New", "ja": "新規", "vi": "Mới"},
|
||||
"co4e.load": {"en": "Load", "ja": "読み込み", "vi": "Tải"},
|
||||
"co4e.delete": {"en": "Delete", "ja": "削除", "vi": "Xóa"},
|
||||
"co4e.edit": {"en": "Edit", "ja": "編集", "vi": "Sửa"},
|
||||
"co4e.new_agent": {"en": "New agent", "ja": "新規エージェント", "vi": "Agent mới"},
|
||||
"co4e.manage_skills": {"en": "Manage skills…", "ja": "スキル管理…", "vi": "Quản lý skill…"},
|
||||
"co4e.template": {"en": "template", "ja": "テンプレート", "vi": "mẫu"},
|
||||
"co4e.saved": {"en": "saved", "ja": "保存済み", "vi": "đã lưu"},
|
||||
"co4e.custom": {"en": "custom", "ja": "カスタム", "vi": "tùy chỉnh"},
|
||||
"co4e.parallel_node": {"en": "Parallel (fan-out)", "ja": "並列(ファンアウト)", "vi": "Song song (fan-out)"},
|
||||
"co4e.add_step": {"en": "Add step", "ja": "ステップ追加", "vi": "Thêm bước"},
|
||||
"co4e.fit": {"en": "Fit", "ja": "全体表示", "vi": "Vừa màn hình"},
|
||||
"co4e.fit_tooltip": {
|
||||
"en": "Auto-fit: zoom to show every step", "ja": "自動フィット:全ステップを表示",
|
||||
"vi": "Tự canh: thu phóng để thấy tất cả bước"},
|
||||
"co4e.drag_hint": {
|
||||
"en": "Drag a flow or agent onto the canvas (double-click a flow to load it).",
|
||||
"ja": "フローやエージェントをキャンバスにドラッグ(フローはダブルクリックで読み込み)。",
|
||||
"vi": "Kéo một flow hoặc agent vào canvas (double-click flow để tải)."},
|
||||
"co4e.blank_step": {"en": "Blank step", "ja": "空のステップ", "vi": "Bước trống"},
|
||||
"co4e.pick_agent": {"en": "Choose an agent", "ja": "エージェントを選択", "vi": "Chọn agent"},
|
||||
"co4e.ai_draft": {"en": "Draft with AI", "ja": "AIで下書き", "vi": "Soạn bằng AI"},
|
||||
"co4e.ai_draft_tooltip": {
|
||||
"en": "Let AI write this agent's instructions from its name and role (no skill needed).",
|
||||
"ja": "エージェントの名前と役割から指示文をAIが作成(スキル不要)。",
|
||||
"vi": "Để AI viết hướng dẫn cho agent từ tên và vai trò (không cần skill)."},
|
||||
"co4e.ai_draft_hint_title": {"en": "Draft with AI", "ja": "AIで下書き", "vi": "Soạn bằng AI"},
|
||||
"co4e.ai_draft_hint_label": {
|
||||
"en": "Describe what this agent should do (optional — leave blank to draft from just "
|
||||
"the name/role). More detail here → more detailed instructions.",
|
||||
"ja": "このエージェントが何をすべきか説明してください(任意 — 空欄なら名前/役割のみから"
|
||||
"下書き)。詳しく書くほど、生成される指示も詳細になります。",
|
||||
"vi": "Mô tả agent này nên làm gì (không bắt buộc — để trống sẽ soạn chỉ từ tên/vai trò). "
|
||||
"Mô tả chi tiết hơn → hướng dẫn được tạo ra chi tiết hơn."},
|
||||
"co4e.tt_add_step": {"en": "Add a blank step to the canvas", "ja": "空のステップをキャンバスに追加",
|
||||
"vi": "Thêm một bước trống vào canvas"},
|
||||
"co4e.tt_save": {"en": "Save this flow", "ja": "このフローを保存", "vi": "Lưu flow này"},
|
||||
"co4e.tt_save_template": {"en": "Save as a reusable template", "ja": "再利用テンプレートとして保存",
|
||||
"vi": "Lưu thành mẫu dùng lại"},
|
||||
"co4e.tt_run": {"en": "Run the flow (or Interrupt while running)", "ja": "フローを実行(実行中は中断)",
|
||||
"vi": "Chạy flow (hoặc Dừng khi đang chạy)"},
|
||||
"co4e.tt_mode": {
|
||||
"en": "Auto = each step plans then runs · Plan = dry-run a plan (read-only) · Manual = step-by-step (advance with Next step)",
|
||||
"ja": "Auto=各ステップが計画して実行 · Plan=計画のみ(読取専用)· Manual=1ステップずつ(「次へ」で進む)",
|
||||
"vi": "Auto = mỗi bước tự lập kế hoạch rồi chạy · Plan = chỉ lập kế hoạch (chỉ đọc) · Manual = từng bước (bấm Bước tiếp)"},
|
||||
"co4e.tt_new_wf": {"en": "Start a new empty flow", "ja": "新しい空のフロー", "vi": "Tạo flow mới trống"},
|
||||
"co4e.tt_load_wf": {"en": "Load the selected flow into the canvas",
|
||||
"ja": "選択したフローをキャンバスに読み込み", "vi": "Tải flow đã chọn vào canvas"},
|
||||
"co4e.tt_del_wf": {"en": "Delete the selected saved flow", "ja": "選択した保存フローを削除",
|
||||
"vi": "Xóa flow đã lưu đang chọn"},
|
||||
"co4e.tt_edit_wf": {"en": "Edit the selected flow", "ja": "選択したフローを編集",
|
||||
"vi": "Sửa flow đang chọn"},
|
||||
"co4e.tt_new_agent": {"en": "Create a custom agent persona", "ja": "カスタムエージェントを作成",
|
||||
"vi": "Tạo một agent tùy chỉnh"},
|
||||
"co4e.tt_edit_agent": {"en": "Edit the selected custom agent", "ja": "選択したカスタムエージェントを編集",
|
||||
"vi": "Sửa agent tùy chỉnh đang chọn"},
|
||||
"co4e.tt_del_agent": {"en": "Delete the selected custom agent", "ja": "選択したカスタムエージェントを削除",
|
||||
"vi": "Xóa agent tùy chỉnh đang chọn"},
|
||||
"co4e.tt_manage_skills": {"en": "Open the Skills manager", "ja": "スキル管理を開く",
|
||||
"vi": "Mở trình quản lý Skill"},
|
||||
"co4e.save": {"en": "Save", "ja": "保存", "vi": "Lưu"},
|
||||
"co4e.save_template": {"en": "Save as Template", "ja": "テンプレートとして保存", "vi": "Lưu làm mẫu"},
|
||||
"co4e.flow_name": {"en": "Flow", "ja": "フロー", "vi": "Flow"},
|
||||
"co4e.run": {"en": "Run", "ja": "実行", "vi": "Chạy"},
|
||||
"co4e.interrupt": {"en": "Interrupt", "ja": "中断", "vi": "Dừng"},
|
||||
"co4e.add": {"en": "Add", "ja": "追加", "vi": "Thêm"},
|
||||
"co4e.config_title": {"en": "Step config", "ja": "ステップ設定", "vi": "Cấu hình bước"},
|
||||
"co4e.tt_collapse_config": {"en": "Collapse the config panel", "ja": "設定パネルを折りたたむ",
|
||||
"vi": "Thu gọn bảng cấu hình"},
|
||||
"co4e.tt_expand_config": {"en": "Expand the config panel", "ja": "設定パネルを展開",
|
||||
"vi": "Mở rộng bảng cấu hình"},
|
||||
"co4e.messages": {"en": "Messages", "ja": "メッセージ", "vi": "Tin nhắn"},
|
||||
"co4e.tt_collapse_msgs": {"en": "Collapse the messages panel", "ja": "メッセージを折りたたむ",
|
||||
"vi": "Thu gọn khung tin nhắn"},
|
||||
"co4e.tt_expand_msgs": {"en": "Expand the messages panel", "ja": "メッセージを展開",
|
||||
"vi": "Mở rộng khung tin nhắn"},
|
||||
"co4e.mode.auto": {"en": "Auto", "ja": "自動", "vi": "Auto"},
|
||||
"co4e.mode.plan": {"en": "Plan", "ja": "計画", "vi": "Plan"},
|
||||
"co4e.mode.manual": {"en": "Manual", "ja": "手動", "vi": "Manual"},
|
||||
# --- Co4E run manager / duplicate / status / zoom (parallel flows) ---
|
||||
"co4e.copy_suffix": {"en": "copy", "ja": "コピー", "vi": "bản sao"},
|
||||
"co4e.tt_dup_wf": {"en": "Duplicate the selected flow (run copies in parallel)",
|
||||
"ja": "選択フローを複製(コピーを並列実行)", "vi": "Nhân bản flow đã chọn (chạy bản sao song song)"},
|
||||
}
|
||||
@@ -0,0 +1,344 @@
|
||||
"""Chuỗi hiển thị — phần login_dialog.
|
||||
|
||||
Cắt ra từ ``i18n.py``: một dict 3.000 dòng không mở nổi để sửa một chữ.
|
||||
Cắt theo mốc phân đoạn có sẵn, nên mỗi cụm vẫn là các màn đi liền nhau.
|
||||
``i18n.py`` gộp tất cả lại thành ``STRINGS``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict
|
||||
|
||||
STRINGS: Dict[str, Dict[str, str]] = {
|
||||
# ---- login_dialog.py: startup login / bootstrap / offline ----
|
||||
"login.title": {"en": "Sign in", "ja": "サインイン", "vi": "Đăng nhập"},
|
||||
"login.header": {"en": "Cowork-Local BamBOO", "ja": "Cowork-Local BamBOO", "vi": "Cowork-Local BamBOO"},
|
||||
"login.account": {"en": "Account", "ja": "アカウント", "vi": "Tài khoản"},
|
||||
"login.code": {"en": "Access code", "ja": "アクセスコード", "vi": "Mã truy cập"},
|
||||
"login.department": {"en": "Department (optional)", "ja": "部署(任意)", "vi": "Phòng ban (không bắt buộc)"},
|
||||
"login.department_placeholder": {
|
||||
"en": "e.g. FA.PDS — groups you automatically", "ja": "例: FA.PDS — 自動でグループ分けされます",
|
||||
"vi": "vd: FA.PDS — sẽ tự động xếp vào nhóm tương ứng"},
|
||||
"login.login_btn": {"en": "Log in", "ja": "ログイン", "vi": "Đăng nhập"},
|
||||
"login.exit_btn": {"en": "Exit", "ja": "終了", "vi": "Thoát"},
|
||||
"login.err_invalid": {
|
||||
"en": "Invalid account or access code.", "ja": "アカウントまたはアクセスコードが無効です。",
|
||||
"vi": "Tài khoản hoặc mã truy cập không đúng."},
|
||||
"login.err_admin_exists": {
|
||||
"en": "An Admin account already exists for this shared folder — the app has exactly one. Log in with an account issued by the Admin instead.",
|
||||
"ja": "この共有フォルダには既に管理者アカウントが存在します(管理者は1人のみ)。管理者から発行されたアカウントでログインしてください。",
|
||||
"vi": "Thư mục dùng chung này đã có tài khoản Admin — app chỉ có duy nhất 1 Admin. Hãy đăng nhập bằng tài khoản do Admin cấp."},
|
||||
"login.err_missing_fields": {
|
||||
"en": "Enter both a shared folder path and an account name.",
|
||||
"ja": "共有フォルダのパスとアカウント名の両方を入力してください。",
|
||||
"vi": "Nhập đường dẫn thư mục chia sẻ và tên tài khoản."},
|
||||
"login.err_shared_dir": {
|
||||
"en": "Could not create the shared folder: {error}",
|
||||
"ja": "共有フォルダを作成できませんでした: {error}",
|
||||
"vi": "Không tạo được thư mục chia sẻ: {error}"},
|
||||
"login.bootstrap_hint": {
|
||||
"en": "No accounts exist yet. Choose a shared folder (a network share or a "
|
||||
"locally-synced OneDrive folder) and create the first Admin account.",
|
||||
"ja": "アカウントがまだありません。共有フォルダ(ネットワーク共有、または同期済みの "
|
||||
"OneDrive フォルダ)を選び、最初の管理者アカウントを作成してください。",
|
||||
"vi": "Chưa có tài khoản nào. Chọn một thư mục chia sẻ (network share hoặc thư mục "
|
||||
"OneDrive đã đồng bộ trên máy) và tạo tài khoản Admin đầu tiên."},
|
||||
"login.shared_dir": {"en": "Shared folder", "ja": "共有フォルダ", "vi": "Thư mục chia sẻ"},
|
||||
"login.browse": {"en": "Browse…", "ja": "参照…", "vi": "Chọn…"},
|
||||
"login.create_admin": {
|
||||
"en": "Create Admin account", "ja": "管理者アカウントを作成", "vi": "Tạo tài khoản Admin"},
|
||||
"login.code_shown_title": {"en": "Admin account created", "ja": "管理者アカウントを作成しました",
|
||||
"vi": "Đã tạo tài khoản Admin"},
|
||||
"login.code_shown_body": {
|
||||
"en": "Account: {username}\nAccess code: {code}\n\nSave this code now — it will "
|
||||
"not be shown again. You are now logged in.",
|
||||
"ja": "アカウント: {username}\nアクセスコード: {code}\n\n今すぐこのコードを保存してくださ"
|
||||
"い — 二度と表示されません。ログインしました。",
|
||||
"vi": "Tài khoản: {username}\nMã truy cập: {code}\n\nHãy lưu lại mã này ngay — mã sẽ "
|
||||
"không hiển thị lại lần nào nữa. Bạn đã đăng nhập."},
|
||||
"login.unreachable": {
|
||||
"en": "Can't reach the shared folder:\n{path}", "ja": "共有フォルダに到達できません:\n{path}",
|
||||
"vi": "Không truy cập được thư mục chia sẻ:\n{path}"},
|
||||
"login.offline_hint": {
|
||||
"en": "Last successful login on this machine: {username} ({role}).",
|
||||
"ja": "このマシンでの最後の正常なログイン: {username} ({role})。",
|
||||
"vi": "Lần đăng nhập thành công gần nhất trên máy này: {username} ({role})."},
|
||||
"login.offline_btn": {"en": "Continue offline as {role}", "ja": "{role} としてオフラインで続行",
|
||||
"vi": "Tiếp tục offline với vai trò {role}"},
|
||||
"login.no_offline_cache": {
|
||||
"en": "No previous successful login on this machine — contact your Admin.",
|
||||
"ja": "このマシンでの過去のログイン履歴がありません — 管理者に連絡してください。",
|
||||
"vi": "Chưa có lượt đăng nhập thành công nào trên máy này — liên hệ Admin."},
|
||||
"login.retry_btn": {"en": "Retry", "ja": "再試行", "vi": "Thử lại"},
|
||||
|
||||
# ---- accounts_tab.py: Monitoring -> Accounts panel (Admin/Sub-admin) --
|
||||
"accounts.edit_title": {"en": "Edit account", "ja": "アカウントを編集", "vi": "Sửa tài khoản"},
|
||||
"accounts.add_title": {"en": "Add account", "ja": "アカウントを追加", "vi": "Thêm tài khoản"},
|
||||
"accounts.f_username": {"en": "Account", "ja": "アカウント", "vi": "Tài khoản"},
|
||||
"accounts.f_display_name": {"en": "Name", "ja": "名前", "vi": "Tên"},
|
||||
"accounts.f_email": {"en": "Email", "ja": "メール", "vi": "Email"},
|
||||
"accounts.f_email_placeholder": {
|
||||
"en": "name@company.com (optional)", "ja": "name@company.com(任意)",
|
||||
"vi": "name@company.com (không bắt buộc)"},
|
||||
"accounts.f_role": {"en": "Role", "ja": "役割", "vi": "Vai trò"},
|
||||
"accounts.f_department": {"en": "Department", "ja": "部門", "vi": "Bộ phận"},
|
||||
"accounts.f_group": {"en": "Group", "ja": "グループ", "vi": "Nhóm"},
|
||||
"accounts.f_group_name": {"en": "Group name", "ja": "グループ名", "vi": "Tên nhóm"},
|
||||
"accounts.no_group": {"en": "— No group —", "ja": "— グループなし —", "vi": "— Không có nhóm —"},
|
||||
"accounts.role.admin": {"en": "Admin", "ja": "管理者", "vi": "Admin"},
|
||||
"accounts.role.subadmin": {"en": "Sub-admin", "ja": "サブ管理者", "vi": "Sub-admin"},
|
||||
"accounts.role.user": {"en": "User", "ja": "ユーザー", "vi": "User"},
|
||||
"accounts.no_shared_dir": {
|
||||
"en": "No shared folder configured — set one in Settings to manage accounts.",
|
||||
"ja": "共有フォルダが設定されていません — 設定でアカウント管理用のフォルダを指定してください。",
|
||||
"vi": "Chưa cấu hình thư mục chia sẻ — thiết lập trong Settings để quản lý tài khoản."},
|
||||
"accounts.shared_dir_hint": {"en": "Shared folder: {path}", "ja": "共有フォルダ: {path}",
|
||||
"vi": "Thư mục chia sẻ: {path}"},
|
||||
"accounts.ungrouped": {"en": "Ungrouped", "ja": "未分類", "vi": "Chưa có nhóm"},
|
||||
"accounts.filter_all_groups": {"en": "All groups", "ja": "すべてのグループ", "vi": "Tất cả nhóm"},
|
||||
"accounts.delete_title": {"en": "Delete account", "ja": "アカウントを削除", "vi": "Xóa tài khoản"},
|
||||
"accounts.delete_confirm": {"en": "Delete account '{username}'?", "ja": "アカウント「{username}」を削"
|
||||
"除しますか?", "vi": "Xóa tài khoản '{username}'?"},
|
||||
"accounts.new_group_title": {"en": "New group", "ja": "新しいグループ", "vi": "Nhóm mới"},
|
||||
"accounts.add_btn": {"en": "Add", "ja": "追加", "vi": "Thêm"},
|
||||
"accounts.edit_btn": {"en": "Edit", "ja": "編集", "vi": "Sửa"},
|
||||
"accounts.delete_btn": {"en": "Delete", "ja": "削除", "vi": "Xóa"},
|
||||
"accounts.generate_code_btn": {"en": "Generate code", "ja": "コード発行", "vi": "Tạo mã"},
|
||||
"accounts.new_group_btn": {"en": "New group", "ja": "新しいグループ", "vi": "Nhóm mới"},
|
||||
"accounts.drag_move_hint": {
|
||||
"en": "Drag an account onto a group to move it there.",
|
||||
"ja": "アカウントをグループにドラッグすると移動できます。",
|
||||
"vi": "Kéo tài khoản thả vào một nhóm để di chuyển đến đó."},
|
||||
"accounts.err_admin_exists": {
|
||||
"en": "An Admin account already exists — the app has exactly one.",
|
||||
"ja": "管理者アカウントは既に存在します(1人のみ)。",
|
||||
"vi": "Đã có tài khoản Admin — app chỉ có duy nhất 1 Admin."},
|
||||
"accounts.search_placeholder": {
|
||||
"en": "Search accounts (or type a question and press )…",
|
||||
"ja": "アカウント検索(質問を入力しても可)…",
|
||||
"vi": "Tìm tài khoản (hoặc gõ câu hỏi rồi bấm )…"},
|
||||
"accounts.ai_search_btn": {"en": "AI", "ja": "AI", "vi": "AI"},
|
||||
"accounts.ai_search_tooltip": {
|
||||
"en": "AI turns your question into a search keyword (e.g. \"who in CAE has no department?\").",
|
||||
"ja": "質問をAIが検索キーワードに変換します。",
|
||||
"vi": "AI chuyển câu hỏi của bạn thành từ khóa tìm kiếm (vd: \"ai trong CAE chưa có phòng ban?\")."},
|
||||
"accounts.excel_template_btn": {
|
||||
"en": "Excel template", "ja": "Excelテンプレート", "vi": "Mẫu Excel"},
|
||||
"accounts.excel_import_btn": {
|
||||
"en": "Import Excel", "ja": "Excel取り込み", "vi": "Nhập từ Excel"},
|
||||
"accounts.excel_imported": {
|
||||
"en": "Created {n} account(s).", "ja": "{n} 件のアカウントを作成しました。",
|
||||
"vi": "Đã tạo {n} tài khoản."},
|
||||
"accounts.excel_codes_saved": {
|
||||
"en": "Access codes saved to: {path}", "ja": "アクセスコードの保存先: {path}",
|
||||
"vi": "Mã truy cập đã lưu tại: {path}"},
|
||||
"accounts.usage_title": {"en": "Usage & Cost by account", "ja": "アカウント別の使用量とコスト",
|
||||
"vi": "Sử dụng & Chi phí theo tài khoản"},
|
||||
"accounts.period.day": {"en": "Day", "ja": "日", "vi": "Ngày"},
|
||||
"accounts.period.week": {"en": "Week", "ja": "週", "vi": "Tuần"},
|
||||
"accounts.period.month": {"en": "Month", "ja": "月", "vi": "Tháng"},
|
||||
"accounts.period.year": {"en": "Year", "ja": "年", "vi": "Năm"},
|
||||
"accounts.col_name": {"en": "Name", "ja": "名前", "vi": "Tên"},
|
||||
"accounts.col_account": {"en": "Account", "ja": "アカウント", "vi": "Tài khoản"},
|
||||
"accounts.col_machine": {"en": "Machine", "ja": "マシン", "vi": "Máy"},
|
||||
"accounts.col_department": {"en": "Department", "ja": "部門", "vi": "Bộ phận"},
|
||||
"accounts.col_tokens": {"en": "Tokens", "ja": "トークン", "vi": "Token"},
|
||||
"accounts.col_cost": {"en": "Cost", "ja": "コスト", "vi": "Chi phí"},
|
||||
|
||||
# ---- app.py: top bar, tabs, toasts, tray ------------------------
|
||||
"app.logo": {"en": "Cowork-Local BamBOO", "ja": "Cowork-Local BamBOO", "vi": "Cowork-Local BamBOO"},
|
||||
"app.provider": {"en": "Provider:", "ja": "プロバイダー:", "vi": "Nhà cung cấp:"},
|
||||
"app.language": {"en": "Language:", "ja": "言語:", "vi": "Ngôn ngữ:"},
|
||||
"app.settings": {"en": "Settings", "ja": "設定", "vi": "Cài đặt"},
|
||||
"app.tab.dashboard": {"en": "Dashboard", "ja": "Dashboard", "vi": "Dashboard"},
|
||||
"app.tab.schedule": {"en": "Schedule Task", "ja": "Schedule Task", "vi": "Schedule Task"},
|
||||
"app.tab.cowork": {"en": "Cowork", "ja": "Cowork", "vi": "Cowork"},
|
||||
"app.tab.code": {"en": "Code", "ja": "Code", "vi": "Code"},
|
||||
"app.tab.structure": {"en": "GraphRAG", "ja": "GraphRAG", "vi": "GraphRAG"},
|
||||
"app.tab.workspace": {"en": "Workspace", "ja": "ワークスペース", "vi": "Workspace"},
|
||||
"app.tab.monitoring": {"en": "Monitoring", "ja": "モニタリング", "vi": "Giám sát"},
|
||||
"app.nav.collapse_tooltip": {"en": "Collapse menu to icons only", "ja": "メニューをアイコンのみに折りたたむ", "vi": "Thu gọn menu về icon"},
|
||||
"app.nav.expand_tooltip": {"en": "Expand menu", "ja": "メニューを展開", "vi": "Mở rộng menu"},
|
||||
"app.nav.menu_label": {"en": "MENU", "ja": "MENU", "vi": "MENU"},
|
||||
# Shown on the rail rows the project gate disables (Cowork, GraphRAG) —
|
||||
# they stay listed and greyed instead of disappearing from the menu.
|
||||
"app.nav.needs_project": {
|
||||
"en": "Select a project first", "ja": "先にプロジェクトを選択してください",
|
||||
"vi": "Chọn project trước"},
|
||||
# Rail header: the project a new chat will be created in, and what to do
|
||||
# when there is no project yet.
|
||||
"app.nav.project_pick": {
|
||||
"en": "Project for new chats", "ja": "新しいチャットのプロジェクト",
|
||||
"vi": "Project cho đoạn chat mới"},
|
||||
"app.nav.no_project": {
|
||||
"en": "No project yet", "ja": "プロジェクトなし", "vi": "Chưa có project"},
|
||||
"app.nav.recents": {"en": "RECENTS", "ja": "最近", "vi": "GẦN ĐÂY"},
|
||||
"app.nav.all_projects": {
|
||||
"en": "All projects…", "ja": "すべてのプロジェクト…", "vi": "Tất cả project…"},
|
||||
"app.nav.create_project_first": {
|
||||
"en": "Create a project first", "ja": "先にプロジェクトを作成してください",
|
||||
"vi": "Tạo project trước"},
|
||||
|
||||
# ---- workspace_tab.py (Projects — Claude-Projects style) -----------
|
||||
"workspace.header": {"en": "Manage projects", "ja": "プロジェクト管理", "vi": "Quản lý project"},
|
||||
"workspace.tab_cowork": {"en": "Cowork", "ja": "Cowork", "vi": "Cowork"},
|
||||
"workspace.tab_graphrag": {"en": "GraphRAG", "ja": "GraphRAG", "vi": "GraphRAG"},
|
||||
"workspace.tab_project": {"en": "Project", "ja": "プロジェクト", "vi": "Project"},
|
||||
"workspace.tab_folder": {"en": "Folder", "ja": "フォルダ", "vi": "Thư mục"},
|
||||
"folder.path_placeholder": {
|
||||
"en": "Folder path", "ja": "フォルダのパス", "vi": "Đường dẫn thư mục"},
|
||||
"folder.open_folder": {"en": "Open folder", "ja": "フォルダを開く", "vi": "Mở thư mục"},
|
||||
"folder.save": {"en": "Save", "ja": "保存", "vi": "Lưu"},
|
||||
"folder.open_external": {
|
||||
"en": "Open externally", "ja": "外部で開く", "vi": "Mở bằng app ngoài"},
|
||||
"folder.preview": {"en": "Preview", "ja": "プレビュー", "vi": "Xem trước"},
|
||||
"folder.edit": {"en": "Edit", "ja": "編集", "vi": "Chỉnh sửa"},
|
||||
"folder.select_file": {
|
||||
"en": "Select a file in the tree to view or edit it.",
|
||||
"ja": "ツリーでファイルを選択して表示・編集します。",
|
||||
"vi": "Chọn một tệp trong cây thư mục để xem hoặc chỉnh sửa."},
|
||||
"folder.binary_file": {
|
||||
"en": "Binary or very large file — open it externally to view.",
|
||||
"ja": "バイナリまたは非常に大きいファイルです — 外部で開いて表示してください。",
|
||||
"vi": "Tệp nhị phân hoặc quá lớn — mở bằng app ngoài để xem."},
|
||||
"folder.converting": {
|
||||
"en": "Rendering document… (converting to PDF via LibreOffice)",
|
||||
"ja": "ドキュメントを表示中…(LibreOffice で PDF に変換しています)",
|
||||
"vi": "Đang hiển thị tài liệu… (chuyển sang PDF bằng LibreOffice)"},
|
||||
"folder.doc_unreadable": {
|
||||
"en": "Could not extract text ({note}). Open it externally for the full document.",
|
||||
"ja": "テキストを抽出できませんでした ({note})。完全な文書は外部で開いてください。",
|
||||
"vi": "Không trích xuất được nội dung ({note}). Mở bằng app ngoài để xem đầy đủ."},
|
||||
"folder.saved": {"en": "Saved {name}", "ja": "{name} を保存しました", "vi": "Đã lưu {name}"},
|
||||
"folder.ai_edit": {"en": "AI Edit", "ja": "AI 編集", "vi": "AI Edit"},
|
||||
"folder.ai_edit_tooltip": {
|
||||
"en": "Edit the open file with AI (uses the Cowork conversation context)",
|
||||
"ja": "AI で開いているファイルを編集(Cowork の会話コンテキストを利用)",
|
||||
"vi": "Dùng AI chỉnh sửa file đang mở (dùng ngữ cảnh hội thoại Cowork)"},
|
||||
"folder.ai_placeholder": {
|
||||
"en": "Describe the edit… (e.g. add error handling)",
|
||||
"ja": "編集内容を入力…(例: エラー処理を追加)",
|
||||
"vi": "Mô tả chỉnh sửa… (vd: thêm xử lý lỗi)"},
|
||||
"folder.ai_send": {"en": "Send", "ja": "送信", "vi": "Gửi"},
|
||||
"folder.ai_no_file": {
|
||||
"en": "Open a text/code file in Edit mode first.",
|
||||
"ja": "先にテキスト/コードファイルを編集モードで開いてください。",
|
||||
"vi": "Hãy mở một file text/code ở chế độ Edit trước."},
|
||||
"folder.ai_applied": {
|
||||
"en": "✓ Applied the edit — review it and Save.",
|
||||
"ja": "✓ 編集を適用しました — 確認して保存してください。",
|
||||
"vi": "✓ Đã áp dụng chỉnh sửa — kiểm tra rồi Lưu."},
|
||||
"folder.ai_empty": {
|
||||
"en": "(the model didn't return an edited file)",
|
||||
"ja": "(モデルは編集後のファイルを返しませんでした)",
|
||||
"vi": "(model không trả về file đã chỉnh sửa)"},
|
||||
"folder.ai_error": {
|
||||
"en": "AI edit failed: {err}", "ja": "AI 編集に失敗しました: {err}",
|
||||
"vi": "AI edit thất bại: {err}"},
|
||||
"folder.ai_running": {
|
||||
"en": "AI is editing {name}… (keeps running while you do other things)",
|
||||
"ja": "AI が {name} を編集中…(他の作業をしていても継続します)",
|
||||
"vi": "AI đang chỉnh sửa {name}… (vẫn chạy tiếp khi bạn làm việc khác)"},
|
||||
"folder.ai_done": {
|
||||
"en": "AI edit finished for {name} — review it in the Folder tab.",
|
||||
"ja": "{name} の AI 編集が完了しました — Folder タブで確認してください。",
|
||||
"vi": "AI edit xong cho {name} — kiểm tra ở tab Folder."},
|
||||
"folder.ai_status_running": {
|
||||
"en": "processing…", "ja": "処理中…", "vi": "đang xử lí…"},
|
||||
"folder.ai_status_done": {
|
||||
"en": "done", "ja": "完了", "vi": "xong"},
|
||||
"folder.ai_planning": {
|
||||
"en": "Planning…", "ja": "計画中…", "vi": "Đang lập kế hoạch…"},
|
||||
"folder.ai_apply": {"en": "Apply", "ja": "適用", "vi": "Áp dụng"},
|
||||
"folder.ai_discard": {"en": "Discard", "ja": "破棄", "vi": "Hủy"},
|
||||
"folder.ai_proposed": {
|
||||
"en": "Proposed changes (review)", "ja": "変更案(確認)",
|
||||
"vi": "Thay đổi đề xuất (xem lại)"},
|
||||
"folder.ai_review_hint": {
|
||||
"en": "Review the diff, then Apply or Discard.",
|
||||
"ja": "差分を確認してから、適用または破棄してください。",
|
||||
"vi": "Xem lại diff rồi bấm Áp dụng hoặc Hủy."},
|
||||
"folder.ai_proposed_status": {
|
||||
"en": "AI proposed an edit for {name} — review & Apply.",
|
||||
"ja": "{name} の編集案が出ました — 確認して適用してください。",
|
||||
"vi": "AI đề xuất chỉnh sửa {name} — xem lại & Áp dụng."},
|
||||
"folder.ai_discarded": {
|
||||
"en": "Discarded — the file was not changed.",
|
||||
"ja": "破棄しました — ファイルは変更されていません。",
|
||||
"vi": "Đã hủy — file không bị thay đổi."},
|
||||
"folder.ai_new_file": {"en": "a new file", "ja": "新規ファイル", "vi": "file mới"},
|
||||
"folder.ai_proposed_new": {
|
||||
"en": "Proposed NEW file: {name} (review)",
|
||||
"ja": "新規ファイルの提案: {name}(確認)",
|
||||
"vi": "Đề xuất tạo file MỚI: {name} (xem lại)"},
|
||||
"folder.ai_created": {
|
||||
"en": "Created {name}", "ja": "{name} を作成しました", "vi": "Đã tạo {name}"},
|
||||
"folder.ai_image_confirm_title": {
|
||||
"en": "Confirm image change", "ja": "画像変更の確認", "vi": "Xác nhận sửa ảnh"},
|
||||
"folder.ai_image_confirm": {
|
||||
"en": "This edit replaces one or more images in the slide. Proceed?",
|
||||
"ja": "この編集はスライド内の画像を置き換えます。実行しますか?",
|
||||
"vi": "Chỉnh sửa này sẽ thay ảnh trong slide. Tiếp tục?"},
|
||||
"folder.ai_image_declined": {
|
||||
"en": "Image change cancelled.", "ja": "画像の変更をキャンセルしました。",
|
||||
"vi": "Đã hủy thay đổi ảnh."},
|
||||
"folder.ai_image_confirm_gen": {
|
||||
"en": "This will GENERATE image(s) with the AI model and save them into the folder. Proceed?",
|
||||
"ja": "AI モデルで画像を生成してフォルダに保存します。実行しますか?",
|
||||
"vi": "Sẽ TẠO ảnh bằng model AI và lưu vào thư mục. Tiếp tục?"},
|
||||
"folder.ai_image_plan": {
|
||||
"en": "Will generate these illustration image(s):",
|
||||
"ja": "以下のイラスト画像を生成します:",
|
||||
"vi": "Sẽ tạo các ảnh minh họa sau:"},
|
||||
"folder.ai_generating": {
|
||||
"en": "Generating image(s)…", "ja": "画像を生成中…", "vi": "Đang tạo ảnh…"},
|
||||
"folder.ai_image_created": {
|
||||
"en": "Generated image {name}", "ja": "画像 {name} を生成しました",
|
||||
"vi": "Đã tạo ảnh {name}"},
|
||||
"folder.ai_image_failed": {
|
||||
"en": "Image generation failed: {err}", "ja": "画像生成に失敗しました: {err}",
|
||||
"vi": "Tạo ảnh thất bại: {err}"},
|
||||
"folder.ai_model_label": {"en": "Model:", "ja": "モデル:", "vi": "Model:"},
|
||||
"folder.ai_model_auto": {
|
||||
"en": "(auto — provider default)", "ja": "(自動 — 既定モデル)",
|
||||
"vi": "(tự động — model mặc định)"},
|
||||
"folder.ai_image_suggest": {
|
||||
"en": "💡 Tip: pick model '{model}' above for image generation.",
|
||||
"ja": "💡 画像生成には上のモデル '{model}' を選ぶのがおすすめです。",
|
||||
"vi": "💡 Gợi ý: chọn model '{model}' ở trên để tạo ảnh."},
|
||||
"folder.ai_image_suggest_all": {
|
||||
"en": "💡 This request involves images. Image-capable models found on other providers:",
|
||||
"ja": "💡 このリクエストは画像を含みます。他プロバイダーで見つかった画像対応モデル:",
|
||||
"vi": "💡 Yêu cầu này liên quan đến ảnh. Model tạo ảnh tìm thấy ở các provider khác:"},
|
||||
"folder.ai_image_none": {
|
||||
"en": "💡 This request involves images, but no image-capable model was found on any configured provider.",
|
||||
"ja": "💡 このリクエストは画像を含みますが、設定済みのどのプロバイダーにも画像対応モデルが見つかりませんでした。",
|
||||
"vi": "💡 Yêu cầu này liên quan đến ảnh, nhưng không tìm thấy model tạo ảnh ở provider nào đã cấu hình."},
|
||||
"folder.ai_image_use_selected": {
|
||||
"en": "💡 No dedicated image model found — will use your selected model '{model}' to generate images.",
|
||||
"ja": "💡 専用の画像モデルが見つかりません — 選択中のモデル '{model}' で画像を生成します。",
|
||||
"vi": "💡 Không tìm thấy model tạo ảnh chuyên biệt — sẽ dùng model bạn đã chọn '{model}' để tạo ảnh."},
|
||||
"folder.ai_queued": {
|
||||
"en": "⏳ Queued (#{n}) — runs after the current edit.",
|
||||
"ja": "⏳ キューに追加 (#{n}) — 現在の編集の後に実行します。",
|
||||
"vi": "⏳ Đã thêm vào hàng đợi (#{n}) — chạy sau lệnh hiện tại."},
|
||||
"folder.ai_queue_count": {
|
||||
"en": "{n} queued", "ja": "{n} 件待機中", "vi": "{n} đang chờ"},
|
||||
"terminal.title": {"en": "Terminal", "ja": "ターミナル", "vi": "Terminal"},
|
||||
"terminal.run": {"en": "Run", "ja": "実行", "vi": "Chạy"},
|
||||
"terminal.placeholder": {
|
||||
"en": "Type a command and press Enter…", "ja": "コマンドを入力して Enter…",
|
||||
"vi": "Nhập lệnh rồi nhấn Enter…"},
|
||||
"terminal.expand_tooltip": {
|
||||
"en": "Expand terminal", "ja": "ターミナルを開く", "vi": "Mở terminal"},
|
||||
"terminal.collapse_tooltip": {
|
||||
"en": "Collapse terminal", "ja": "ターミナルを閉じる", "vi": "Thu gọn terminal"},
|
||||
"terminal.busy": {
|
||||
"en": "[a command is still running]", "ja": "[コマンドがまだ実行中です]",
|
||||
"vi": "[đang chạy một lệnh khác]"},
|
||||
"terminal.cd_error": {
|
||||
"en": "cd: no such directory: {path}", "ja": "cd: ディレクトリがありません: {path}",
|
||||
"vi": "cd: không có thư mục: {path}"},
|
||||
"terminal.launch_error": {
|
||||
"en": "[failed to launch the shell]", "ja": "[シェルの起動に失敗しました]",
|
||||
"vi": "[không khởi chạy được shell]"},
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
"""Chuỗi hiển thị — phần monitoring_overview.
|
||||
|
||||
Cắt ra từ ``i18n.py``: một dict 3.000 dòng không mở nổi để sửa một chữ.
|
||||
Cắt theo mốc phân đoạn có sẵn, nên mỗi cụm vẫn là các màn đi liền nhau.
|
||||
``i18n.py`` gộp tất cả lại thành ``STRINGS``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict
|
||||
|
||||
STRINGS: Dict[str, Dict[str, str]] = {
|
||||
"monitoring.overview_sandbox_id": {"en": "Sandbox ID", "ja": "サンドボックス ID", "vi": "Sandbox ID"},
|
||||
"monitoring.overview_status": {"en": "Status", "ja": "状態", "vi": "Trạng thái"},
|
||||
"monitoring.overview_status_running": {"en": "Running", "ja": "実行中", "vi": "Đang chạy"},
|
||||
"monitoring.overview_created": {"en": "Created", "ja": "作成日時", "vi": "Tạo lúc"},
|
||||
"monitoring.overview_uptime": {"en": "Uptime", "ja": "稼働時間", "vi": "Thời gian hoạt động"},
|
||||
"monitoring.overview_resource_limits": {
|
||||
"en": "Resource Limits", "ja": "リソース制限", "vi": "Giới hạn tài nguyên"},
|
||||
"monitoring.overview_edit": {"en": "Edit", "ja": "編集", "vi": "Sửa"},
|
||||
"monitoring.overview_network_label": {"en": "Network", "ja": "ネットワーク", "vi": "Mạng"},
|
||||
"monitoring.overview_network_disabled": {"en": "Disabled", "ja": "無効", "vi": "Đã tắt"},
|
||||
"monitoring.overview_network_enabled": {"en": "Enabled", "ja": "有効", "vi": "Đang mở"},
|
||||
"monitoring.overview_permissions_title": {"en": "Permissions", "ja": "権限", "vi": "Quyền"},
|
||||
"monitoring.overview_perm_fs": {"en": "File System", "ja": "ファイルシステム", "vi": "Hệ thống file"},
|
||||
"monitoring.overview_perm_fs_value": {"en": "Read/Write", "ja": "読み書き", "vi": "Đọc/Ghi"},
|
||||
"monitoring.overview_perm_network": {"en": "Network", "ja": "ネットワーク", "vi": "Mạng"},
|
||||
"monitoring.overview_perm_network_blocked": {"en": "Blocked", "ja": "ブロック", "vi": "Bị chặn"},
|
||||
"monitoring.overview_perm_network_allowed": {"en": "Allowed", "ja": "許可", "vi": "Cho phép"},
|
||||
"monitoring.overview_perm_process": {"en": "Process", "ja": "プロセス", "vi": "Tiến trình"},
|
||||
"monitoring.overview_perm_process_value": {"en": "Limited", "ja": "制限あり", "vi": "Bị hạn chế"},
|
||||
"monitoring.overview_perm_env": {"en": "Environment", "ja": "実行環境", "vi": "Môi trường"},
|
||||
"monitoring.overview_perm_env_value": {"en": "Restricted", "ja": "制限あり", "vi": "Bị giới hạn"},
|
||||
"monitoring.overview_audit_title": {"en": "Audit Log", "ja": "監査ログ", "vi": "Audit Log"},
|
||||
"monitoring.overview_view_all": {"en": "View all", "ja": "すべて表示", "vi": "Xem tất cả"},
|
||||
"monitoring.time_just_now": {"en": "just now", "ja": "たった今", "vi": "vừa xong"},
|
||||
"monitoring.time_minutes_ago": {"en": "{n}m ago", "ja": "{n}分前", "vi": "{n} phút trước"},
|
||||
"monitoring.time_hours_ago": {"en": "{n}h ago", "ja": "{n}時間前", "vi": "{n} giờ trước"},
|
||||
"monitoring.time_days_ago": {"en": "{n}d ago", "ja": "{n}日前", "vi": "{n} ngày trước"},
|
||||
"monitoring.na": {"en": "—", "ja": "—", "vi": "—"},
|
||||
}
|
||||
@@ -0,0 +1,343 @@
|
||||
"""Chuỗi hiển thị — phần settings_dialog.
|
||||
|
||||
Cắt ra từ ``i18n.py``: một dict 3.000 dòng không mở nổi để sửa một chữ.
|
||||
Cắt theo mốc phân đoạn có sẵn, nên mỗi cụm vẫn là các màn đi liền nhau.
|
||||
``i18n.py`` gộp tất cả lại thành ``STRINGS``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict
|
||||
|
||||
STRINGS: Dict[str, Dict[str, str]] = {
|
||||
"settings.sandbox_block_network": {
|
||||
"en": "Block network for agent-run commands",
|
||||
"ja": "エージェントが実行するコマンドのネットワークをブロック",
|
||||
"vi": "Chặn mạng cho lệnh do agent chạy"},
|
||||
"settings.allow_url_fetch": {
|
||||
"en": "Allow the agent to fetch URLs (web pages, SharePoint / OneDrive links)",
|
||||
"ja": "エージェントによるURL取得を許可(Webページ、SharePoint / OneDriveリンク)",
|
||||
"vi": "Cho phép agent lấy dữ liệu từ URL (trang web, link SharePoint / OneDrive)"},
|
||||
"settings.allow_url_fetch_tooltip": {
|
||||
"en": ("Lets the agent's fetch_url tool read web pages, online documents and "
|
||||
"SharePoint/OneDrive share links to search & process them. Separate from "
|
||||
"'Block network' (which only sandboxes shell commands). Default: on."),
|
||||
"ja": "エージェントのfetch_urlツールがWebページ・オンライン文書・SharePoint/OneDrive共有リンクを"
|
||||
"読み取れるようにします。「ネットワークをブロック」(シェルコマンド用)とは別です。既定: オン。",
|
||||
"vi": ("Cho phép tool fetch_url của agent đọc trang web, tài liệu online và link chia sẻ "
|
||||
"SharePoint/OneDrive để tìm kiếm & xử lý. Tách biệt với 'Chặn mạng' (chỉ áp cho lệnh "
|
||||
"shell). Mặc định: bật.")},
|
||||
"settings.test_internet": {
|
||||
"en": "Test Internet", "ja": "インターネット接続テスト", "vi": "Kiểm tra Internet"},
|
||||
"settings.test_internet_tooltip": {
|
||||
"en": ("Live-checks the app's own outbound HTTPS path (the same one fetch_url uses) "
|
||||
"and reports the concrete reason if it can't reach the internet."),
|
||||
"ja": "アプリ自身の送信HTTPS経路(fetch_urlと同じ)を実際にテストし、インターネットに到達できない"
|
||||
"場合は具体的な理由を表示します。",
|
||||
"vi": ("Kiểm tra trực tiếp đường HTTPS ra ngoài của app (đúng đường mà fetch_url dùng) và "
|
||||
"báo lý do cụ thể nếu không truy cập được internet.")},
|
||||
"settings.testing_internet": {
|
||||
"en": "Testing internet access…", "ja": "インターネット接続をテスト中…",
|
||||
"vi": "Đang kiểm tra truy cập internet…"},
|
||||
"settings.sandbox_block_network_tooltip": {
|
||||
"en": ("Policy-level control (proxy env vars point at a black hole) — not a kernel "
|
||||
"firewall. Combine with the command whitelist above for defense in depth."),
|
||||
"ja": "ポリシーレベルの制御です(プロキシ環境変数をブラックホールに向ける)— カーネルレベルの"
|
||||
"ファイアウォールではありません。上のコマンドホワイトリストと併用してください。",
|
||||
"vi": "Kiểm soát ở tầng chính sách (trỏ biến môi trường proxy vào hố đen) — không phải "
|
||||
"firewall tầng kernel. Kết hợp với whitelist lệnh ở trên để phòng thủ nhiều lớp."},
|
||||
"settings.sandbox_unlimited": {"en": "Unlimited", "ja": "無制限", "vi": "Không giới hạn"},
|
||||
"settings.sandbox_cpu_label": {"en": "CPU limit", "ja": "CPU 制限", "vi": "Giới hạn CPU"},
|
||||
"settings.sandbox_memory_label": {"en": "Memory limit", "ja": "メモリ制限", "vi": "Giới hạn bộ nhớ"},
|
||||
"settings.sandbox_disk_label": {"en": "Disk I/O limit", "ja": "ディスク I/O 制限", "vi": "Giới hạn disk I/O"},
|
||||
"settings.sandbox_hint": {
|
||||
"en": ("Applies to every run_command/install_package the agent executes "
|
||||
"(Cowork, Code tab, and Schedule Task alike). 0 = unlimited. This layer is "
|
||||
"independent of \"Agent Security\" above — it still applies even while that "
|
||||
"toggle is off."),
|
||||
"ja": "エージェントが実行するすべての run_command/install_package に適用されます"
|
||||
"(Cowork、Code タブ、Schedule Task 共通)。0 = 無制限。この機能は上の「Agent "
|
||||
"Security」とは独立しており、そのトグルがオフの間も適用され続けます。",
|
||||
"vi": "Áp dụng cho mọi run_command/install_package mà agent chạy (Cowork, tab Code, "
|
||||
"và Schedule Task). 0 = không giới hạn. Lớp này độc lập với \"Agent Security\" "
|
||||
"ở trên — vẫn áp dụng ngay cả khi tắt Agent Security."},
|
||||
"settings.group.mcp": {"en": "MCP Servers", "ja": "MCP サーバー", "vi": "MCP Servers"},
|
||||
"settings.mcp_hint": {
|
||||
"en": ("Connect to external MCP (Model Context Protocol) servers — e.g. the official "
|
||||
"filesystem/GitHub/brave-search servers — and their tools become available to "
|
||||
"the agent alongside Microsoft 365 and the built-in file/command tools."),
|
||||
"ja": "外部の MCP(Model Context Protocol)サーバー(公式の filesystem/GitHub/brave-search "
|
||||
"サーバーなど)に接続すると、そのツールが Microsoft 365 や組み込みのファイル/コマンド"
|
||||
"ツールと並んでエージェントから利用できるようになります。",
|
||||
"vi": "Kết nối tới các MCP server bên ngoài (vd: server filesystem/GitHub/brave-search chính "
|
||||
"thức) — tool của chúng sẽ khả dụng cho agent cùng với Microsoft 365 và tool file/lệnh "
|
||||
"có sẵn."},
|
||||
"settings.mcp_add_btn": {"en": "Add server…", "ja": "サーバーを追加…", "vi": "Thêm server…"},
|
||||
"settings.mcp_edit_btn": {"en": "Edit", "ja": "編集", "vi": "Sửa"},
|
||||
"settings.mcp_delete_btn": {"en": "Delete", "ja": "削除", "vi": "Xóa"},
|
||||
"settings.mcp_no_servers": {
|
||||
"en": "(No MCP servers configured — click 'Add server…')",
|
||||
"ja": "(MCP サーバーが設定されていません。「サーバーを追加…」をクリック)",
|
||||
"vi": "(Chưa cấu hình MCP server nào — bấm 'Thêm server…')"},
|
||||
"settings.mcp_delete_confirm": {
|
||||
"en": "Remove MCP server \"{name}\"?", "ja": "MCP サーバー「{name}」を削除しますか?",
|
||||
"vi": "Xóa MCP server \"{name}\"?"},
|
||||
"mcp.add_title": {"en": "Add MCP server", "ja": "MCP サーバーを追加", "vi": "Thêm MCP server"},
|
||||
"mcp.edit_title": {"en": "Edit MCP server", "ja": "MCP サーバーを編集", "vi": "Sửa MCP server"},
|
||||
"mcp.name_label": {"en": "Name", "ja": "名前", "vi": "Tên"},
|
||||
"mcp.name_placeholder": {"en": "e.g. filesystem", "ja": "例: filesystem", "vi": "vd: filesystem"},
|
||||
"mcp.command_label": {"en": "Command", "ja": "コマンド", "vi": "Lệnh"},
|
||||
"mcp.command_placeholder": {"en": "e.g. npx", "ja": "例: npx", "vi": "vd: npx"},
|
||||
"mcp.args_label": {"en": "Arguments", "ja": "引数", "vi": "Tham số"},
|
||||
"mcp.args_placeholder": {
|
||||
"en": "e.g. -y @modelcontextprotocol/server-filesystem C:\\Data",
|
||||
"ja": "例: -y @modelcontextprotocol/server-filesystem C:\\Data",
|
||||
"vi": "vd: -y @modelcontextprotocol/server-filesystem C:\\Data"},
|
||||
"mcp.hint": {
|
||||
"en": ("The server is launched as a subprocess and talked to over stdio (the standard "
|
||||
"MCP transport) — the SAME way Claude Desktop/other MCP clients connect to it."),
|
||||
"ja": "サーバーはサブプロセスとして起動され、stdio(標準の MCP トランスポート)で通信します"
|
||||
"— Claude Desktop など他の MCP クライアントと同じ方式です。",
|
||||
"vi": "Server được khởi chạy như 1 subprocess và giao tiếp qua stdio (giao thức MCP chuẩn) "
|
||||
"— giống cách Claude Desktop hay các MCP client khác kết nối tới nó."},
|
||||
|
||||
# ---- settings_dialog.py / ext_connector_dialog.py: External Connectors (CAD/CAE/Office) ----
|
||||
"settings.group.ext": {
|
||||
"en": "Connectors (MCP)",
|
||||
"ja": "コネクタ(MCP)",
|
||||
"vi": "Connectors (MCP)"},
|
||||
"settings.ext_moved_hint": {
|
||||
"en": "Connector (MCP / REST-API) setup moved to Monitoring → Tools → Connector.",
|
||||
"ja": "コネクター(MCP / REST-API)の設定は「モニタリング → ツール → Connector」へ移動しました。",
|
||||
"vi": "Thiết lập Connector (MCP / REST-API) đã chuyển sang Monitoring → Công cụ → Connector."},
|
||||
"settings.ext_hint": {
|
||||
"en": ("One place for every external tool source — grouped as CAD (NX/CATIA/SolidWorks/"
|
||||
"AutoCAD), CAE (ANSA/ABAQUS/HyperWorks/ANSYS), MS365 (Microsoft 365/OneDrive/"
|
||||
"SharePoint) and Other (any generic MCP server). MS365 auto-connects via the built-in "
|
||||
"server once you sign in; for the rest, point each connector at an MCP server you "
|
||||
"already have or a REST API it exposes (no vendor SDK is bundled)."),
|
||||
"ja": "外部ツール接続を1か所に集約 — CAD(NX/CATIA/SolidWorks/AutoCAD)、CAE(ANSA/ABAQUS/"
|
||||
"HyperWorks/ANSYS)、MS365(Microsoft 365/OneDrive/SharePoint)、Other(汎用 MCP サーバー)。"
|
||||
"MS365 はサインインすると内蔵サーバーで自動接続。その他は既存の MCP サーバーまたは REST API を"
|
||||
"指定してください(ベンダー SDK は同梱しません)。",
|
||||
"vi": "Một nơi duy nhất cho mọi nguồn tool ngoài — nhóm theo CAD (NX/CATIA/SolidWorks/AutoCAD), "
|
||||
"CAE (ANSA/ABAQUS/HyperWorks/ANSYS), MS365 (Microsoft 365/OneDrive/SharePoint) và Other "
|
||||
"(MCP server bất kỳ). MS365 tự kết nối qua server tích hợp sau khi đăng nhập; còn lại bạn "
|
||||
"trỏ mỗi connector tới MCP server bạn đã có hoặc REST API nó cung cấp (không kèm SDK hãng nào)."},
|
||||
"settings.ext_add_btn": {"en": "Add connector…", "ja": "コネクタを追加…", "vi": "Thêm connector…"},
|
||||
"settings.ext_edit_btn": {"en": "Edit", "ja": "編集", "vi": "Sửa"},
|
||||
"settings.ext_delete_btn": {"en": "Delete", "ja": "削除", "vi": "Xóa"},
|
||||
"settings.ext_delete_confirm": {
|
||||
"en": "Remove connector \"{name}\"?", "ja": "コネクタ「{name}」を削除しますか?",
|
||||
"vi": "Xóa connector \"{name}\"?"},
|
||||
"ext.add_title": {"en": "Add connector", "ja": "コネクタを追加", "vi": "Thêm connector"},
|
||||
"ext.edit_title": {"en": "Edit connector", "ja": "コネクタを編集", "vi": "Sửa connector"},
|
||||
"ext.category_label": {"en": "Category", "ja": "カテゴリ", "vi": "Nhóm"},
|
||||
"ext.preset_label": {"en": "App", "ja": "アプリ", "vi": "Ứng dụng"},
|
||||
"ext.preset_custom": {"en": "(Custom…)", "ja": "(カスタム…)", "vi": "(Tuỳ chỉnh…)"},
|
||||
"ext.name_label": {"en": "Display name", "ja": "表示名", "vi": "Tên hiển thị"},
|
||||
"ext.name_placeholder": {"en": "e.g. NX (Site A)", "ja": "例: NX(サイトA)", "vi": "vd: NX (Site A)"},
|
||||
"ext.mode_label": {"en": "Connection type", "ja": "接続方式", "vi": "Kiểu kết nối"},
|
||||
"ext.mode_mcp": {"en": "MCP server (stdio)", "ja": "MCP サーバー(stdio)", "vi": "MCP server (stdio)"},
|
||||
"ext.mode_rest": {"en": "REST API", "ja": "REST API", "vi": "REST API"},
|
||||
"ext.mode_builtin": {"en": "built-in, auto-connect", "ja": "内蔵・自動接続", "vi": "tích hợp, tự kết nối"},
|
||||
"settings.ms365_signin_btn": {"en": "Sign in to Microsoft 365", "ja": "Microsoft 365 にサインイン",
|
||||
"vi": "Đăng nhập Microsoft 365"},
|
||||
"settings.ms365_signout_btn": {"en": "Sign out", "ja": "サインアウト", "vi": "Đăng xuất"},
|
||||
"settings.ms365_signed_in": {"en": "Microsoft 365: signed in as {who}",
|
||||
"ja": "Microsoft 365: {who} でサインイン中",
|
||||
"vi": "Microsoft 365: đã đăng nhập ({who})"},
|
||||
"settings.ms365_signed_out": {
|
||||
"en": "Microsoft 365: not signed in — one click, no Tenant/Client ID needed.",
|
||||
"ja": "Microsoft 365: 未サインイン — ワンクリック、テナント/クライアント ID 不要。",
|
||||
"vi": "Microsoft 365: chưa đăng nhập — 1 cú click, không cần Tenant/Client ID."},
|
||||
"settings.ms365_signing_in": {
|
||||
"en": "Microsoft 365: opening sign-in… follow the code prompt.",
|
||||
"ja": "Microsoft 365: サインインを開始中… コードの案内に従ってください。",
|
||||
"vi": "Microsoft 365: đang mở đăng nhập… làm theo hướng dẫn mã code."},
|
||||
"settings.ms365_code_hint": {
|
||||
"en": ("The sign-in page opened in your browser (<a href='{url}'>{url}</a>) and the code "
|
||||
"below was copied to your clipboard — just paste it, then sign in with your Microsoft "
|
||||
"account. This window closes automatically when sign-in completes."),
|
||||
"ja": ("ブラウザでサインインページ(<a href='{url}'>{url}</a>)を開きました。下のコードはクリップボードに"
|
||||
"コピー済みです — 貼り付けて Microsoft アカウントでサインインしてください。完了すると自動で閉じます。"),
|
||||
"vi": ("Trang đăng nhập đã mở trong trình duyệt (<a href='{url}'>{url}</a>) và mã bên dưới đã được "
|
||||
"copy vào clipboard — chỉ cần dán, rồi đăng nhập bằng tài khoản Microsoft. Cửa sổ này tự đóng "
|
||||
"khi đăng nhập xong.")},
|
||||
"settings.ms365_copy_code": {"en": "Copy code", "ja": "コードをコピー", "vi": "Copy mã"},
|
||||
"settings.ms365_open_link": {"en": "Open link", "ja": "リンクを開く", "vi": "Mở link"},
|
||||
"settings.ms365_local_connected": {
|
||||
"en": "OneDrive / SharePoint: auto-connected via local sync — no sign-in needed.\nSynced folder: {path}",
|
||||
"ja": "OneDrive / SharePoint: ローカル同期で自動接続 — サインイン不要。\n同期フォルダ: {path}",
|
||||
"vi": "OneDrive / SharePoint: tự động kết nối qua thư mục sync local — không cần đăng nhập.\nThư mục đã sync: {path}"},
|
||||
"settings.ms365_local_none": {
|
||||
"en": "OneDrive / SharePoint: no locally-synced OneDrive folder found. Install/sign in to "
|
||||
"the OneDrive desktop app and sync a folder, then reopen Settings.",
|
||||
"ja": "OneDrive / SharePoint: ローカル同期の OneDrive フォルダが見つかりません。OneDrive デスクトップ"
|
||||
"アプリでサインインしフォルダを同期してから、設定を開き直してください。",
|
||||
"vi": "OneDrive / SharePoint: chưa tìm thấy thư mục OneDrive sync trên máy. Cài/đăng nhập OneDrive "
|
||||
"desktop và sync một thư mục, rồi mở lại Settings."},
|
||||
"ext.command_label": {"en": "Command", "ja": "コマンド", "vi": "Lệnh"},
|
||||
"ext.command_placeholder": {"en": "e.g. python or npx", "ja": "例: python または npx", "vi": "vd: python hoặc npx"},
|
||||
"ext.args_label": {"en": "Arguments", "ja": "引数", "vi": "Tham số"},
|
||||
"ext.args_placeholder": {"en": "e.g. -m nx_mcp_server", "ja": "例: -m nx_mcp_server", "vi": "vd: -m nx_mcp_server"},
|
||||
"ext.base_url_label": {"en": "Base URL", "ja": "ベース URL", "vi": "Base URL"},
|
||||
"ext.base_url_placeholder": {
|
||||
"en": "e.g. https://cad-api.internal.company.com",
|
||||
"ja": "例: https://cad-api.internal.company.com",
|
||||
"vi": "vd: https://cad-api.internal.company.com"},
|
||||
"ext.api_key_label": {"en": "API key", "ja": "API キー", "vi": "API key"},
|
||||
"ext.auth_header_label": {"en": "Auth header name", "ja": "認証ヘッダー名", "vi": "Tên header xác thực"},
|
||||
"ext.auth_scheme_label": {"en": "Auth scheme", "ja": "認証スキーム", "vi": "Auth scheme"},
|
||||
"ext.test_btn": {"en": "Test connection", "ja": "接続テスト", "vi": "Kiểm tra kết nối"},
|
||||
"ext.err_no_command": {
|
||||
"en": "Enter a command first.", "ja": "先にコマンドを入力してください。", "vi": "Hãy nhập lệnh trước."},
|
||||
"ext.test_mcp_ok": {
|
||||
"en": "MCP server started and responded.", "ja": "MCP サーバーが起動し応答しました。",
|
||||
"vi": "MCP server đã khởi chạy và phản hồi."},
|
||||
|
||||
"settings.sec_enabled": {
|
||||
"en": "Enable AI-assisted agent security guardrails",
|
||||
"ja": "AI 支援のエージェント セキュリティ ガードレールを有効化",
|
||||
"vi": "Bật các lớp bảo mật agent có AI hỗ trợ"},
|
||||
"settings.sec_hint": {
|
||||
"en": "Three independent layers: an AI reviews the user's request and "
|
||||
"attachment content against the rules below before the agent "
|
||||
"acts, and a whitelist + AI control-agent checks every "
|
||||
"run_command/install_package call. A violation always blocks "
|
||||
"the action and emails the admin below. Each AI check fails "
|
||||
"OPEN (allows) if the model itself can't be reached — a gateway "
|
||||
"hiccup must never make the agent unusable.",
|
||||
"ja": "3つの独立した層があります:エージェントが行動する前に、AI がユーザーの"
|
||||
"リクエストと添付ファイルの内容を下記のルールと照合してチェックし、"
|
||||
"ホワイトリストと AI コントロールエージェントがすべての "
|
||||
"run_command/install_package 呼び出しをチェックします。違反時は常に"
|
||||
"操作をブロックし、下記の管理者にメールで通知します。各 AI チェックは"
|
||||
"モデルに到達できない場合は「許可」側に倒れます(フェイルオープン)— "
|
||||
"ゲートウェイの一時的な不調でエージェントが使えなくなることがあっては"
|
||||
"なりません。",
|
||||
"vi": "Ba lớp độc lập: AI kiểm tra yêu cầu của người dùng và nội dung file "
|
||||
"đính kèm theo các rule bên dưới TRƯỚC khi agent hành động, và một "
|
||||
"whitelist + AI control-agent kiểm tra mọi lệnh run_command/"
|
||||
"install_package. Vi phạm sẽ luôn CHẶN hành động và gửi email cho "
|
||||
"admin bên dưới. Mỗi lớp kiểm tra bằng AI sẽ MẶC ĐỊNH CHO PHÉP nếu "
|
||||
"không gọi được model — một sự cố gateway tạm thời không được phép "
|
||||
"làm agent ngừng hoạt động."},
|
||||
"settings.sec_validate_prompt": {
|
||||
"en": "Validate the user's request (prompt) before acting",
|
||||
"ja": "行動する前にユーザーのリクエスト(プロンプト)を検証",
|
||||
"vi": "Validate yêu cầu (prompt) của người dùng trước khi hành động"},
|
||||
"settings.sec_validate_attachments": {
|
||||
"en": "Scan attachment/file content for malicious payloads",
|
||||
"ja": "添付/ファイルの内容に悪意あるペイロードがないかスキャン",
|
||||
"vi": "Scan nội dung file đính kèm để phát hiện nội dung độc hại"},
|
||||
"settings.sec_validate_commands": {
|
||||
"en": "Check run_command / install_package against a whitelist",
|
||||
"ja": "run_command / install_package をホワイトリストと照合",
|
||||
"vi": "Kiểm tra run_command / install_package theo whitelist"},
|
||||
"settings.sec_command_ai_check": {
|
||||
"en": "Also let an AI control-agent judge commands not covered by the whitelist",
|
||||
"ja": "ホワイトリストに含まれないコマンドは AI コントロールエージェントにも判定させる",
|
||||
"vi": "Cho AI control-agent xét thêm các lệnh whitelist chưa liệt kê"},
|
||||
"settings.sec_whitelist_label": {"en": "Command whitelist", "ja": "コマンド ホワイトリスト", "vi": "Whitelist lệnh"},
|
||||
"settings.sec_whitelist_placeholder": {
|
||||
"en": "One regex pattern per line, e.g. ^pip install\\n^pytest\\n^git ",
|
||||
"ja": "1行に1つの正規表現、例: ^pip install\\n^pytest\\n^git ",
|
||||
"vi": "Mỗi dòng 1 regex, vd: ^pip install\\n^pytest\\n^git "},
|
||||
"settings.sec_whitelist_empty_warning": {
|
||||
"en": "Empty whitelist + AI check off = every command is BLOCKED "
|
||||
"(fail-closed) — add a pattern above or turn AI check back on.",
|
||||
"ja": "ホワイトリストが空でAIチェックも無効の場合、すべてのコマンドが"
|
||||
"ブロックされます(フェイルクローズ)。上にパターンを追加するか"
|
||||
"AIチェックを再度有効にしてください。",
|
||||
"vi": "Whitelist trống + tắt AI-check = MỌI lệnh sẽ bị CHẶN hết "
|
||||
"(fail-closed) — hãy thêm pattern ở trên hoặc bật lại AI-check."},
|
||||
"settings.sec_onedrive_label": {"en": "OneDrive rules link", "ja": "OneDrive ルールへのリンク", "vi": "Link OneDrive chứa rule"},
|
||||
"settings.sec_onedrive_placeholder": {
|
||||
"en": "(optional) sharing link to an admin-authored .md rules document",
|
||||
"ja": "(任意)管理者が作成した .md ルール文書への共有リンク",
|
||||
"vi": "(tuỳ chọn) link chia sẻ tới file .md rule do admin soạn"},
|
||||
"settings.sec_admin_email_label": {"en": "Admin email", "ja": "管理者メール", "vi": "Email admin"},
|
||||
"settings.sec_admin_email_placeholder": {
|
||||
"en": "admin@yourcompany.com — receives violation alerts via Microsoft 365",
|
||||
"ja": "admin@yourcompany.com — Microsoft 365 経由で違反アラートを受信",
|
||||
"vi": "admin@yourcompany.com — nhận cảnh báo vi phạm qua Microsoft 365"},
|
||||
"settings.sec_rules_path_hint": {
|
||||
"en": "Local admin rules file (optional, edited directly, always applied): {path}",
|
||||
"ja": "ローカルの管理者ルールファイル(任意・直接編集・常に適用): {path}",
|
||||
"vi": "File rule admin cục bộ (tuỳ chọn, sửa trực tiếp, luôn được áp dụng): {path}"},
|
||||
"settings.group.history": {"en": "Conversation history", "ja": "会話履歴", "vi": "Lịch sử hội thoại"},
|
||||
"settings.history_local": {"en": "Local (this PC)", "ja": "ローカル(このPC)", "vi": "Local (máy này)"},
|
||||
"settings.history_onedrive": {"en": "OneDrive", "ja": "OneDrive", "vi": "OneDrive"},
|
||||
"settings.location": {"en": "Location", "ja": "保存先", "vi": "Nơi lưu"},
|
||||
"settings.folder": {"en": "Folder", "ja": "フォルダ", "vi": "Thư mục"},
|
||||
"settings.folder_placeholder": {
|
||||
"en": "(optional) specific folder — leave empty for default",
|
||||
"ja": "(任意)特定のフォルダ ― 空欄で既定値",
|
||||
"vi": "(tuỳ chọn) thư mục cụ thể — để trống dùng mặc định"},
|
||||
"settings.browse": {"en": "Browse…", "ja": "参照…", "vi": "Duyệt…"},
|
||||
"settings.autosave": {"en": "Auto-save history after each turn", "ja": "各ターン後に履歴を自動保存", "vi": "Tự động lưu lịch sử sau mỗi lượt"},
|
||||
"settings.group.cowork": {"en": "Cowork", "ja": "Cowork", "vi": "Cowork"},
|
||||
"settings.max_parallel": {"en": "Max parallel conversations", "ja": "同時実行する会話数の上限", "vi": "Số hội thoại chạy song song tối đa"},
|
||||
"settings.parallel_suffix": {"en": " conversations at once", "ja": " 件を同時実行", "vi": " hội thoại cùng lúc"},
|
||||
"settings.parallel_tooltip": {
|
||||
"en": ("How many conversations run in parallel. Within one conversation messages always "
|
||||
"run one at a time (queued); only different conversations run in parallel."),
|
||||
"ja": ("並列実行する会話数です。1つの会話内のメッセージは常に1件ずつ(キュー)実行され、"
|
||||
"異なる会話同士のみ並列に実行されます。"),
|
||||
"vi": ("Số cuộc trò chuyện chạy song song. Trong MỘT cuộc trò chuyện, tin nhắn luôn "
|
||||
"chạy lần lượt (xếp hàng) để không bị trộn lẫn; chỉ các cuộc trò chuyện khác "
|
||||
"nhau mới chạy song song.")},
|
||||
"settings.group.attachments": {"en": "Attachments", "ja": "添付ファイル", "vi": "Tệp đính kèm"},
|
||||
"settings.max_files": {"en": "Max files", "ja": "最大ファイル数", "vi": "Số tệp tối đa"},
|
||||
"settings.max_files_suffix": {"en": " files / message", "ja": " 件 / メッセージ", "vi": " tệp / tin nhắn"},
|
||||
"settings.max_files_tooltip": {
|
||||
"en": "Maximum number of files attachable to one message.",
|
||||
"ja": "1メッセージに添付できるファイル数の上限。",
|
||||
"vi": "Số tệp tối đa đính kèm vào một tin nhắn."},
|
||||
"settings.max_per_file": {"en": "Max per file", "ja": "ファイルあたりの上限", "vi": "Giới hạn mỗi tệp"},
|
||||
"settings.max_per_file_suffix": {"en": " K tokens / file", "ja": " Kトークン / ファイル", "vi": " K tokens / tệp"},
|
||||
"settings.max_per_file_tooltip": {
|
||||
"en": ("Limits how much of each attached file's content is added to the prompt; anything "
|
||||
"beyond this is truncated (fewer tokens, avoids exceeding the context limit)."),
|
||||
"ja": "各添付ファイルの内容をプロンプトに含める量の上限。超過分は切り捨てられます(トークン削減、コンテキスト超過回避)。",
|
||||
"vi": ("Giới hạn nội dung mỗi tệp đính kèm đưa vào prompt; phần vượt sẽ bị cắt "
|
||||
"(giảm token, tránh lỗi vượt context).")},
|
||||
"settings.group.structure": {"en": "GraphRAG", "ja": "GraphRAG", "vi": "GraphRAG"},
|
||||
"settings.group.sandbox_limits": {"en": "Sandbox resource limits", "ja": "サンドボックスのリソース上限",
|
||||
"vi": "Giới hạn tài nguyên Sandbox"},
|
||||
"settings.max_nodes": {"en": "Max nodes", "ja": "最大ノード数", "vi": "Số node tối đa"},
|
||||
"settings.unlimited": {"en": "Unlimited", "ja": "無制限", "vi": "Không giới hạn"},
|
||||
"settings.nodes_suffix": {"en": " nodes", "ja": " ノード", "vi": " node"},
|
||||
"settings.nodes_tooltip": {
|
||||
"en": ("Cap the number of nodes in the Structure graph (0 = unlimited). "
|
||||
"A lower cap speeds up scanning/layout for large folders."),
|
||||
"ja": "構造グラフのノード数上限(0=無制限)。大きなフォルダでは低い値の方が高速です。",
|
||||
"vi": "Giới hạn số node trong đồ thị Cấu trúc (0 = không giới hạn). Giá trị thấp hơn giúp quét/vẽ nhanh hơn với thư mục lớn."},
|
||||
"settings.max_edges": {"en": "Max edges", "ja": "最大エッジ数", "vi": "Số cạnh tối đa"},
|
||||
"settings.edges_suffix": {"en": " edges", "ja": " エッジ", "vi": " cạnh"},
|
||||
"settings.edges_tooltip": {
|
||||
"en": "Cap the number of edges in the Structure graph (0 = unlimited).",
|
||||
"ja": "構造グラフのエッジ数上限(0=無制限)。",
|
||||
"vi": "Giới hạn số cạnh trong đồ thị Cấu trúc (0 = không giới hạn)."},
|
||||
"settings.tip": {
|
||||
"en": "Tip: set your Internal Gateway URL + API key above, then pick a model.",
|
||||
"ja": "ヒント: 上で社内ゲートウェイの URL と API キーを設定してからモデルを選んでください。",
|
||||
"vi": "Mẹo: điền URL Gateway nội bộ + API key ở trên, rồi chọn model."},
|
||||
"settings.loading_models": {"en": "Loading models…", "ja": "モデルを読み込み中…", "vi": "Đang tải danh sách model…"},
|
||||
"settings.loaded_models": {
|
||||
"en": "Loaded {n} model(s) for {provider}.", "ja": "{provider} のモデルを {n} 件読み込みました。",
|
||||
"vi": "Đã tải {n} model cho {provider}."},
|
||||
"settings.load_failed": {"en": "Load failed: {err}", "ja": "読み込み失敗: {err}", "vi": "Tải thất bại: {err}"},
|
||||
"settings.load_models_error": {
|
||||
"en": "No models loaded — {err}", "ja": "モデルを読み込めませんでした — {err}",
|
||||
"vi": "Không tải được model nào — {err}"},
|
||||
"settings.load_models_error_unknown": {
|
||||
"en": "unknown error (check base URL / API key / network).",
|
||||
"ja": "不明なエラー(URL・APIキー・ネットワークを確認)。",
|
||||
"vi": "lỗi không xác định (kiểm tra base URL / API key / kết nối mạng)."},
|
||||
"settings.test_connection": {"en": "Test connection", "ja": "接続テスト", "vi": "Test kết nối"},
|
||||
"settings.test_connection_tooltip": {
|
||||
"en": "Check connectivity to this provider right now and show the real reason if it fails.",
|
||||
"ja": "このプロバイダーへの接続を今すぐ確認し、失敗した場合は本当の理由を表示します。",
|
||||
"vi": "Kiểm tra kết nối tới provider này ngay và hiện lý do thật nếu thất bại."},
|
||||
}
|
||||
+342
@@ -0,0 +1,342 @@
|
||||
"""Chuỗi hiển thị — phần sidebar.
|
||||
|
||||
Cắt ra từ ``i18n.py``: một dict 3.000 dòng không mở nổi để sửa một chữ.
|
||||
Cắt theo mốc phân đoạn có sẵn, nên mỗi cụm vẫn là các màn đi liền nhau.
|
||||
``i18n.py`` gộp tất cả lại thành ``STRINGS``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict
|
||||
|
||||
STRINGS: Dict[str, Dict[str, str]] = {
|
||||
"terminal.exit": {
|
||||
"en": "[process exited with code {code}]", "ja": "[プロセス終了 コード {code}]",
|
||||
"vi": "[tiến trình kết thúc, mã {code}]"},
|
||||
"folder.save_error": {
|
||||
"en": "Save failed: {err}", "ja": "保存に失敗しました: {err}", "vi": "Lưu thất bại: {err}"},
|
||||
|
||||
"workspace.hint": {
|
||||
"en": ("Group chats into projects. Every thread in a project follows the shared "
|
||||
"Instructions, works inside the project's own sandbox folder, and auto-reads "
|
||||
"files placed at that folder's root (project knowledge)."),
|
||||
"ja": ("チャットをプロジェクトにまとめます。プロジェクト内の各スレッドは共有の指示に従い、"
|
||||
"プロジェクト専用のサンドボックスフォルダ内で動作し、そのルートに置かれたファイル"
|
||||
"(プロジェクトナレッジ)を自動的に読み込みます。"),
|
||||
"vi": ("Gom các cuộc chat thành project. Mọi thread trong một project tuân theo phần "
|
||||
"Instructions chung, làm việc trong thư mục sandbox riêng của project, và tự đọc "
|
||||
"các file đặt ở gốc thư mục đó (project knowledge)."),
|
||||
},
|
||||
"workspace.new_project": {"en": "New project", "ja": "新規プロジェクト", "vi": "Project mới"},
|
||||
"workspace.projects_heading": {"en": "PROJECTS", "ja": "プロジェクト", "vi": "PROJECT"},
|
||||
"workspace.folder_label": {"en": "Workspace folder", "ja": "作業フォルダ", "vi": "Thư mục làm việc"},
|
||||
"workspace.counts": {
|
||||
"en": "{chats} chats · {tasks} tasks",
|
||||
"ja": "チャット {chats} · タスク {tasks}",
|
||||
"vi": "{chats} đoạn chat · {tasks} task"},
|
||||
"workspace.delete": {"en": "Delete", "ja": "削除", "vi": "Xóa"},
|
||||
"workspace.delete_confirm": {
|
||||
"en": "Delete project “{name}”? Its conversations and files are kept (threads move to General).",
|
||||
"ja": "プロジェクト「{name}」を削除しますか?会話とファイルは保持されます(スレッドは General へ移動)。",
|
||||
"vi": "Xóa project “{name}”? Hội thoại và file vẫn được giữ (thread chuyển về General).",
|
||||
},
|
||||
"workspace.deleted": {"en": "Deleted project {name}.", "ja": "プロジェクト {name} を削除しました。", "vi": "Đã xóa project {name}."},
|
||||
"workspace.conversation_project_missing": {
|
||||
"en": "This conversation's project no longer exists — it can't be opened.",
|
||||
"ja": "この会話のプロジェクトは既に存在しないため開けません。",
|
||||
"vi": "Project của hội thoại này không còn tồn tại — không thể mở."},
|
||||
"workspace.name": {"en": "Name", "ja": "名前", "vi": "Tên"},
|
||||
"workspace.description": {"en": "Description", "ja": "説明", "vi": "Mô tả"},
|
||||
"workspace.instructions": {"en": "Instructions", "ja": "Instructions", "vi": "Instructions"},
|
||||
"workspace.instructions_placeholder": {
|
||||
"en": "e.g. \"All answers in Vietnamese. We are building the X reporting tool; always follow the naming rules …\"",
|
||||
"ja": "例:「回答はすべて日本語で。X レポートツールを開発中。命名規則に従うこと …」",
|
||||
"vi": "vd: \"Trả lời bằng tiếng Việt. Team đang xây tool báo cáo X; luôn theo quy tắc đặt tên …\"",
|
||||
},
|
||||
"workspace.browse": {"en": "Change", "ja": "変更", "vi": "Đổi"},
|
||||
"workspace.browse_tooltip": {
|
||||
"en": "Choose the project's workspace folder (agent sandbox + shared knowledge root)",
|
||||
"ja": "プロジェクトのワークスペースフォルダを選択(エージェントのサンドボックス+共有ナレッジのルート)",
|
||||
"vi": "Chọn thư mục workspace của project (sandbox của agent + gốc chứa knowledge chung)",
|
||||
},
|
||||
"workspace.open_folder": {"en": "Open", "ja": "開く", "vi": "Mở"},
|
||||
"workspace.save": {"en": "Save project", "ja": "プロジェクトを保存", "vi": "Lưu project"},
|
||||
"workspace.saved": {"en": "Saved project {name}.", "ja": "プロジェクト {name} を保存しました。", "vi": "Đã lưu project {name}."},
|
||||
"workspace.threads": {"en": "Conversations in this project", "ja": "このプロジェクトの会話", "vi": "Hội thoại trong project này"},
|
||||
"workspace.new_chat": {"en": "New chat in this project", "ja": "このプロジェクトで新規チャット", "vi": "Chat mới trong project này"},
|
||||
"workspace.default_new_name": {"en": "New project", "ja": "新規プロジェクト", "vi": "Project mới"},
|
||||
"workspace.collapse_projects_tooltip": {"en": "Collapse the project list", "ja": "プロジェクト一覧を折りたたむ", "vi": "Thu gọn danh sách project"},
|
||||
"workspace.expand_projects_tooltip": {"en": "Click to expand the project list", "ja": "クリックしてプロジェクト一覧を展開", "vi": "Bấm để mở rộng danh sách project"},
|
||||
"app.status.ready": {"en": "Ready.", "ja": "準備完了。", "vi": "Sẵn sàng."},
|
||||
"app.status.using_provider": {"en": "Using {label}.", "ja": "{label} を使用中。", "vi": "Đang dùng {label}."},
|
||||
"app.status.settings_saved": {"en": "Settings saved.", "ja": "設定を保存しました。", "vi": "Đã lưu cài đặt."},
|
||||
"app.credit": {"en": "Made by QuanDH14", "ja": "Made by QuanDH14", "vi": "Made by QuanDH14"},
|
||||
"app.tray.open": {"en": "Open Cowork Local", "ja": "Cowork Local を開く", "vi": "Mở Cowork Local"},
|
||||
"app.tray.quit": {"en": "Quit", "ja": "終了", "vi": "Thoát"},
|
||||
"app.tray.running_body": {
|
||||
"en": "Running in the background — tasks keep working. Right-click the tray icon to Quit.",
|
||||
"ja": "バックグラウンドで実行中です。タスクは継続します。終了するにはトレイアイコンを右クリックしてください。",
|
||||
"vi": "Đang chạy nền — tác vụ vẫn tiếp tục. Chuột phải vào biểu tượng khay để Thoát.",
|
||||
},
|
||||
"app.toast.done": {"en": "{name}: done", "ja": "{name}: 完了", "vi": "{name}: hoàn thành"},
|
||||
"app.toast.error": {"en": "{name}: error", "ja": "{name}: エラー", "vi": "{name}: lỗi"},
|
||||
"app.toast.task_done": {"en": "Task done: {title}", "ja": "タスク完了: {title}",
|
||||
"vi": "Task hoàn thành: {title}"},
|
||||
"app.toast.task_failed": {"en": "Task failed: {title}", "ja": "タスク失敗: {title}",
|
||||
"vi": "Task lỗi: {title}"},
|
||||
|
||||
# ---- sidebar.py (History) ----------------------------------------
|
||||
"sidebar.header": {"en": "History", "ja": "履歴", "vi": "Lịch sử"},
|
||||
"sidebar.filter.all": {"en": "All", "ja": "すべて", "vi": "Tất cả"},
|
||||
"sidebar.filter.cowork": {"en": "Cowork", "ja": "Cowork", "vi": "Cowork"},
|
||||
"sidebar.filter.code": {"en": "Code", "ja": "Code", "vi": "Code"},
|
||||
"sidebar.search_placeholder": {
|
||||
"en": "Search by title or content…", "ja": "タイトルまたは内容で検索…",
|
||||
"vi": "Tìm theo tiêu đề hoặc nội dung…"},
|
||||
"sidebar.search_tooltip": {
|
||||
"en": "Search conversation history by title or message content.",
|
||||
"ja": "会話履歴をタイトルまたはメッセージ内容で検索します。",
|
||||
"vi": "Tìm kiếm lịch sử hội thoại theo tiêu đề hoặc nội dung tin nhắn."},
|
||||
"sidebar.no_matches": {"en": "(no matches)", "ja": "(一致なし)", "vi": "(không tìm thấy)"},
|
||||
"sidebar.refresh": {"en": "Refresh", "ja": "更新", "vi": "Làm mới"},
|
||||
"sidebar.refresh_tooltip": {
|
||||
"en": "Update the list + this conversation's agent status",
|
||||
"ja": "一覧とこの会話のエージェント状態を更新",
|
||||
"vi": "Cập nhật danh sách + trạng thái agent của hội thoại đang xem",
|
||||
},
|
||||
"sidebar.empty": {"en": "(empty)", "ja": "(空)", "vi": "(trống)"},
|
||||
"sidebar.running_suffix": {"en": " · running", "ja": " · 実行中", "vi": " · đang chạy"},
|
||||
"sidebar.expand_tooltip": {
|
||||
"en": "Click to expand the History panel", "ja": "クリックして履歴パネルを展開",
|
||||
"vi": "Bấm để mở lại bảng Lịch sử"},
|
||||
"sidebar.collapse_tooltip": {
|
||||
"en": "Collapse the History panel", "ja": "履歴パネルを折りたたむ",
|
||||
"vi": "Thu gọn bảng Lịch sử"},
|
||||
"sidebar.menu.pin": {"en": "Pin", "ja": "ピン留め", "vi": "Ghim"},
|
||||
"sidebar.menu.unpin": {"en": "Unpin", "ja": "ピン留め解除", "vi": "Bỏ ghim"},
|
||||
"sidebar.menu.rename": {"en": "Rename…", "ja": "名前を変更…", "vi": "Đổi tên…"},
|
||||
"sidebar.menu.delete": {"en": "Delete", "ja": "削除", "vi": "Xóa"},
|
||||
"sidebar.rename.title": {"en": "Rename conversation", "ja": "会話の名前を変更", "vi": "Đổi tên hội thoại"},
|
||||
"sidebar.rename.label": {"en": "New name:", "ja": "新しい名前:", "vi": "Tên mới:"},
|
||||
"sidebar.delete.title": {"en": "Delete conversation", "ja": "会話を削除", "vi": "Xóa hội thoại"},
|
||||
"sidebar.delete.confirm": {"en": "Delete '{title}'?", "ja": "「{title}」を削除しますか?", "vi": "Xóa '{title}'?"},
|
||||
"sidebar.menu.delete_selected": {"en": "Delete {n} selected", "ja": "選択した{n}件を削除", "vi": "Xóa {n} mục đã chọn"},
|
||||
"sidebar.delete_multi.confirm": {
|
||||
"en": "Delete {n} selected conversations? This cannot be undone.",
|
||||
"ja": "選択した{n}件の会話を削除しますか?元に戻せません。",
|
||||
"vi": "Xóa {n} hội thoại đã chọn? Không thể hoàn tác."},
|
||||
|
||||
# ---- widgets.py (Plan / Files sections, collapse strips) ---------
|
||||
"widgets.plan_title": {"en": "Plan", "ja": "プラン", "vi": "Plan"},
|
||||
"widgets.input_files": {"en": "Input files", "ja": "入力ファイル", "vi": "Tệp đầu vào"},
|
||||
"widgets.output_files": {"en": "Output files", "ja": "出力ファイル", "vi": "Tệp đầu ra"},
|
||||
|
||||
# ---- chat_view.py --------------------------------------------------
|
||||
"chat.running": {"en": "Running", "ja": "実行中", "vi": "Đang chạy"},
|
||||
"chat.thinking": {"en": "Thinking", "ja": "思考中", "vi": "Đang nghĩ"},
|
||||
"chat.creating": {"en": "Creating", "ja": "作成中", "vi": "Đang tạo"},
|
||||
"chat.editing": {"en": "Editing", "ja": "編集中", "vi": "Đang sửa"},
|
||||
"chat.installing": {"en": "Installing", "ja": "インストール中", "vi": "Đang cài đặt"},
|
||||
"chat.reading": {"en": "Reading", "ja": "読み込み中", "vi": "Đang đọc"},
|
||||
"chat.you": {"en": "You", "ja": "あなた", "vi": "Bạn"},
|
||||
"chat.assistant": {"en": "Assistant", "ja": "アシスタント", "vi": "Assistant"},
|
||||
"chat.error": {"en": "Error", "ja": "エラー", "vi": "Lỗi"},
|
||||
"help_agent.title": {
|
||||
# The audit page names this AI Assistant, and keeps it the same in every
|
||||
# language — it is a product name, not a description.
|
||||
"en": "AI Assistant", "ja": "AI Assistant", "vi": "AI Assistant"},
|
||||
"help_agent.greeting": {
|
||||
"en": "Hello {name}, have a great working day! How can I help you use the app?",
|
||||
"ja": "こんにちは {name} さん、良い一日を!アプリの使い方について何かお手伝いできますか?",
|
||||
"vi": "Xin chào {name}, chúc bạn một ngày làm việc vui vẻ! Mình có thể giúp gì cho bạn khi dùng app?"},
|
||||
"help_agent.default_user": {"en": "Admin", "ja": "Admin", "vi": "Admin"},
|
||||
"help_agent.placeholder": {
|
||||
"en": "Ask how to use the app…", "ja": "アプリの使い方を質問…",
|
||||
"vi": "Hỏi cách sử dụng app…"},
|
||||
"help_agent.open_tooltip": {
|
||||
"en": "AI Assistant — help using the app",
|
||||
"ja": "AI Assistant — アプリの使い方をサポート",
|
||||
"vi": "AI Assistant — hỗ trợ sử dụng app"},
|
||||
"help_agent.collapse_tooltip": {
|
||||
"en": "Minimize", "ja": "最小化", "vi": "Thu nhỏ"},
|
||||
"help_agent.hide_tooltip": {
|
||||
"en": "Hide to the edge", "ja": "端に隠す", "vi": "Ẩn vào cạnh phải"},
|
||||
"help_agent.dot_hint": {
|
||||
"en": "right-click to hide",
|
||||
"ja": "右クリックで非表示",
|
||||
"vi": "chuột phải để ẩn"},
|
||||
# The name on the launcher pill. Deliberately the same in every language —
|
||||
# it is a product name, and it only shows on hover, so length is not a
|
||||
# constraint the way it was on a permanently visible badge.
|
||||
"help_agent.badge": {
|
||||
"en": "AI Assistant", "ja": "AI Assistant", "vi": "AI Assistant"},
|
||||
"help_agent.more_tooltip": {
|
||||
"en": "More", "ja": "その他", "vi": "Thêm"},
|
||||
"help_agent.show_tooltip": {
|
||||
"en": "Show the AI Assistant", "ja": "AI Assistant を表示",
|
||||
"vi": "Hiện AI Assistant"},
|
||||
"help_agent.empty_reply": {
|
||||
"en": "(no answer)", "ja": "(回答なし)", "vi": "(không có phản hồi)"},
|
||||
"help_agent.error": {
|
||||
"en": "Sorry, I couldn't answer right now: {error}",
|
||||
"ja": "申し訳ありません、今は回答できませんでした: {error}",
|
||||
"vi": "Xin lỗi, hiện chưa thể trả lời: {error}"},
|
||||
"chat.model_switched": {
|
||||
"en": "↻ Auto-switched to {model} — re-checking the previous step, then continuing.",
|
||||
"ja": "↻ {model} に自動切り替え — 直前のステップを確認してから続行します。",
|
||||
"vi": "↻ Đã tự động chuyển sang {model} — kiểm tra lại bước trước rồi tiếp tục."},
|
||||
"chat.provider_default_short": {
|
||||
"en": "the provider's default model", "ja": "プロバイダー既定のモデル",
|
||||
"vi": "model mặc định của provider"},
|
||||
"chat.delete_link": {"en": "Delete", "ja": "削除", "vi": "Xóa"},
|
||||
"chat.delete_tooltip": {
|
||||
"en": "Delete this message and its input/output files",
|
||||
"ja": "このメッセージと入出力ファイルを削除",
|
||||
"vi": "Xóa tin nhắn này và các tệp input/output của nó"},
|
||||
"chat.open_workspace": {"en": "Open workspace", "ja": "作業フォルダを開く", "vi": "Mở thư mục làm việc"},
|
||||
"chat.open_folder": {"en": "Open folder", "ja": "フォルダを開く", "vi": "Mở thư mục"},
|
||||
"chat.open_output_folder": {"en": "Open output folder", "ja": "出力フォルダを開く", "vi": "Mở thư mục output"},
|
||||
"chat.done_marker": {"en": "Done", "ja": "完了しました", "vi": "Đã hoàn thành"},
|
||||
"chat.session_folder_marker": {
|
||||
"en": "This conversation's output folder", "ja": "この会話の出力フォルダ",
|
||||
"vi": "Thư mục output của hội thoại này"},
|
||||
"chat.open_folder_short": {"en": "Open folder", "ja": "フォルダを開く", "vi": "Mở thư mục"},
|
||||
"chat.diff_before": {"en": "Before", "ja": "編集前", "vi": "Trước khi sửa"},
|
||||
"chat.diff_after": {"en": "After", "ja": "編集後", "vi": "Sau khi sửa"},
|
||||
"chat.diff_added": {"en": "Added", "ja": "追加", "vi": "Thêm mới"},
|
||||
"chat.diff_removed": {"en": "Removed", "ja": "削除", "vi": "Đã xóa"},
|
||||
"chat.attachment_warning_title": {
|
||||
"en": "Attachment", "ja": "添付ファイル", "vi": "Tệp đính kèm"},
|
||||
"chat.attachment_failed": {
|
||||
"en": "Could not read \"{name}\": {note}",
|
||||
"ja": "「{name}」を読み込めませんでした: {note}",
|
||||
"vi": "Không đọc được nội dung \"{name}\": {note}"},
|
||||
"chat.reading_progress": {
|
||||
"en": "Reading {name} — page {page}/{total}…",
|
||||
"ja": "{name} を読み込み中 — {page}/{total} ページ…",
|
||||
"vi": "Đang đọc {name} — trang {page}/{total}…"},
|
||||
"chat.workspace_files_capped": {
|
||||
"en": "Folder has more files than the per-message limit — loaded {shown}/{total} (raise it in Settings → Attachments)",
|
||||
"ja": "フォルダ内のファイル数が1メッセージあたりの上限を超えています — {shown}/{total} 件を読み込みました( 設定 → 添付ファイルで変更可)",
|
||||
"vi": "Thư mục có nhiều file hơn giới hạn mỗi tin nhắn — đã đọc {shown}/{total} file (đổi trong Settings → Attachments)"},
|
||||
|
||||
# ---- chat_panel.py (shared by Cowork & Code) ----------------------
|
||||
"chatpanel.agent_label": {"en": "Agent:", "ja": "エージェント:", "vi": "Agent:"},
|
||||
# ---- Auto Model Assessment & Routing (core/routing/) -----------------
|
||||
"routing.toggle_label": {"en": "Routing:", "ja": "ルーティング:", "vi": "Định tuyến:"},
|
||||
"routing.autorun_label": {"en": "Auto-run", "ja": "自動実行", "vi": "Tự chạy"},
|
||||
"routing.autorun_tooltip": {
|
||||
"en": "Auto-approve commands in THIS workspace (no confirm dialog).\nUnchecked: ask before each command. Each workspace keeps its own setting.",
|
||||
"ja": "このワークスペースでコマンドを自動承認(確認なし)。\nオフ: 実行前に確認。ワークスペースごとに設定を保持します。",
|
||||
"vi": "Tự động duyệt lệnh trong workspace NÀY (không hỏi xác nhận).\nBỏ chọn: hỏi trước mỗi lệnh. Mỗi workspace giữ thiết lập riêng.",
|
||||
},
|
||||
"routing.mode_off": {"en": "Off", "ja": "オフ", "vi": "Tắt"},
|
||||
"routing.mode_auto": {"en": "Auto", "ja": "自動", "vi": "Tự động"},
|
||||
"routing.mode_manual": {"en": "Manual", "ja": "手動", "vi": "Thủ công"},
|
||||
# Fallback (R03-T03): resilience mode -- never switches for a better
|
||||
# score, only to rescue a selected model that cannot serve the turn.
|
||||
"routing.mode_fallback": {"en": "Fallback", "ja": "フォールバック", "vi": "Dự phòng"},
|
||||
"routing.toggle_tooltip": {
|
||||
"en": "Auto model routing for this chat.\nOff: always use the selected model.\nAuto: silently switch to the best-fit model.\nManual: ask before switching.\nFallback: keep the selected model, switch only if it is unavailable.",
|
||||
"ja": "このチャットの自動モデルルーティング。\nオフ: 選択したモデルを常に使用。\n自動: 最適なモデルへ自動切替。\n手動: 切替前に確認。\nフォールバック: 選択モデルを維持し、利用できない場合のみ切替。",
|
||||
"vi": "Tự động định tuyến model cho khung chat này.\nTắt: luôn dùng model đã chọn.\nTự động: tự chuyển sang model phù hợp nhất.\nThủ công: hỏi xác nhận trước khi chuyển.\nDự phòng: giữ model đã chọn, chỉ chuyển khi model đó không dùng được.",
|
||||
},
|
||||
"routing.confirm_title": {
|
||||
"en": "Switch model?", "ja": "モデルを切り替えますか?", "vi": "Chuyển model?",
|
||||
},
|
||||
"routing.confirm_body": {
|
||||
"en": "A better-fit model was found for this {task} task:\n\n{from_model} → {to_model}\n(fit gain +{gain})\n\n{reason}\n\nSwitch to it for this message?",
|
||||
"ja": "この {task} タスクにより適したモデルが見つかりました:\n\n{from_model} → {to_model}\n(適合度 +{gain})\n\n{reason}\n\nこのメッセージで切り替えますか?",
|
||||
"vi": "Đã tìm thấy model phù hợp hơn cho tác vụ {task} này:\n\n{from_model} → {to_model}\n(điểm phù hợp +{gain})\n\n{reason}\n\nChuyển sang model đó cho tin nhắn này?",
|
||||
},
|
||||
"routing.confirm_yes": {"en": "Switch", "ja": "切り替える", "vi": "Chuyển"},
|
||||
"routing.confirm_no": {"en": "Keep current", "ja": "現状維持", "vi": "Giữ nguyên"},
|
||||
"routing.confirm_countdown": {
|
||||
"en": "Keep current ({secs}s)", "ja": "現状維持 ({secs}秒)", "vi": "Giữ nguyên ({secs}s)",
|
||||
},
|
||||
"routing.switched_notice": {
|
||||
"en": "↪ Auto-routed to {model} ({task}, fit +{gain})",
|
||||
"ja": "↪ {model} へ自動ルーティング ({task}, 適合度 +{gain})",
|
||||
"vi": "↪ Đã tự chuyển sang {model} ({task}, phù hợp +{gain})",
|
||||
},
|
||||
"routing.reassessing": {
|
||||
"en": "Assessing models…", "ja": "モデルを評価中…", "vi": "Đang đánh giá model…",
|
||||
},
|
||||
"routing.reassess_done": {
|
||||
"en": "Model assessment complete: {count} model(s) scored.",
|
||||
"ja": "モデル評価完了: {count} 件を採点しました。",
|
||||
"vi": "Đánh giá model xong: đã chấm {count} model.",
|
||||
},
|
||||
# ---- Routing settings group (settings_dialog.py) ---------------------
|
||||
"routing.settings_group": {
|
||||
"en": "Auto Model Routing", "ja": "自動モデルルーティング", "vi": "Tự động định tuyến Model",
|
||||
},
|
||||
"routing.settings_mode": {"en": "Default mode", "ja": "既定モード", "vi": "Chế độ mặc định"},
|
||||
"routing.settings_policy": {"en": "Policy", "ja": "ポリシー", "vi": "Chính sách"},
|
||||
"routing.policy_quality": {"en": "Quality", "ja": "品質", "vi": "Chất lượng"},
|
||||
"routing.policy_cost": {"en": "Cost", "ja": "コスト", "vi": "Chi phí"},
|
||||
"routing.policy_latency": {"en": "Latency", "ja": "レイテンシ", "vi": "Độ trễ"},
|
||||
"routing.policy_balanced": {"en": "Balanced", "ja": "バランス", "vi": "Cân bằng"},
|
||||
"routing.settings_min_gain": {
|
||||
"en": "Min score gain to switch", "ja": "切替に必要な最小スコア差", "vi": "Chênh điểm tối thiểu để chuyển",
|
||||
},
|
||||
"routing.settings_timeout": {
|
||||
"en": "Confirm timeout (sec)", "ja": "確認タイムアウト (秒)", "vi": "Thời gian chờ xác nhận (giây)",
|
||||
},
|
||||
"routing.settings_interval": {
|
||||
"en": "Reassess every (hours, 0=off)", "ja": "再評価間隔 (時間, 0=無効)", "vi": "Đánh giá lại mỗi (giờ, 0=tắt)",
|
||||
},
|
||||
"routing.settings_concurrency": {
|
||||
"en": "Max probe calls per provider", "ja": "プロバイダーごとの最大プローブ数", "vi": "Số lần probe tối đa mỗi provider",
|
||||
},
|
||||
"routing.settings_judge": {
|
||||
"en": "Judge model (blank = auto)", "ja": "ジャッジモデル (空欄=自動)", "vi": "Model chấm điểm (trống = tự động)",
|
||||
},
|
||||
"routing.settings_reassess_now": {
|
||||
"en": "Reassess now", "ja": "今すぐ再評価", "vi": "Đánh giá lại ngay",
|
||||
},
|
||||
"routing.settings_hint": {
|
||||
"en": "The app benchmarks each model and routes chats to the best-fit one. Probing spends tokens, so it runs on a schedule / when you add a model / when you click Reassess.",
|
||||
"ja": "各モデルをベンチマークし、最適なモデルへチャットを振り分けます。プローブはトークンを消費するため、スケジュール・モデル追加時・「再評価」押下時のみ実行されます。",
|
||||
"vi": "Ứng dụng benchmark từng model và định tuyến chat tới model phù hợp nhất. Probe tốn token nên chỉ chạy theo lịch / khi thêm model / khi bấm Đánh giá lại.",
|
||||
},
|
||||
"chatpanel.menu_open": {"en": "Open", "ja": "開く", "vi": "Mở"},
|
||||
"chatpanel.menu_ai_edit": {"en": "View & AI edit", "ja": "表示 & AI編集", "vi": "Xem & sửa bằng AI"},
|
||||
# ---- file_edit_dialog.py (view file + AI edit) -----------------------
|
||||
"fileedit.title": {"en": "View & edit file", "ja": "ファイル表示・編集", "vi": "Xem & sửa file"},
|
||||
"fileedit.browse_tooltip": {"en": "Open another file…", "ja": "別のファイルを開く…",
|
||||
"vi": "Mở file khác…"},
|
||||
"fileedit.reload_tooltip": {"en": "Reload from disk", "ja": "ディスクから再読み込み",
|
||||
"vi": "Tải lại từ đĩa"},
|
||||
"fileedit.pick_hint": {"en": "Open a file to view or edit it.",
|
||||
"ja": "表示・編集するファイルを開いてください。",
|
||||
"vi": "Mở một file để xem hoặc chỉnh sửa."},
|
||||
"fileedit.instruction_placeholder": {
|
||||
"en": "Tell the AI how to edit this file (e.g. 'fix typos', 'translate to English')…",
|
||||
"ja": "このファイルの編集内容をAIに指示(例:「誤字修正」「英語に翻訳」)…",
|
||||
"vi": "Nói cho AI cách sửa file này (vd: 'sửa lỗi chính tả', 'dịch sang tiếng Anh')…"},
|
||||
"fileedit.ai_btn": {"en": "AI Edit", "ja": "AI編集", "vi": "Sửa bằng AI"},
|
||||
"fileedit.save_btn": {"en": "Save", "ja": "保存", "vi": "Lưu"},
|
||||
"fileedit.close_btn": {"en": "Close", "ja": "閉じる", "vi": "Đóng"},
|
||||
"fileedit.loaded_editable": {"en": "Text file — editable.", "ja": "テキストファイル — 編集可能。",
|
||||
"vi": "File văn bản — có thể sửa."},
|
||||
"fileedit.loaded_readonly": {
|
||||
"en": "Binary/large document — extracted text shown, read-only (view & ask only).",
|
||||
"ja": "バイナリ/大きい文書 — 抽出テキストを表示(閲覧のみ、編集不可)。",
|
||||
"vi": "Tài liệu nhị phân/lớn — hiển thị text trích xuất, chỉ đọc (chỉ xem & hỏi)."},
|
||||
"fileedit.not_found": {"en": "File not found: {path}", "ja": "ファイルが見つかりません: {path}",
|
||||
"vi": "Không tìm thấy file: {path}"},
|
||||
"fileedit.needs_instruction": {"en": "Enter an edit instruction first.",
|
||||
"ja": "先に編集指示を入力してください。",
|
||||
"vi": "Hãy nhập yêu cầu chỉnh sửa trước."},
|
||||
"fileedit.ai_working": {"en": "AI is editing…", "ja": "AIが編集中…", "vi": "AI đang chỉnh sửa…"},
|
||||
"fileedit.ai_done": {"en": "AI edit applied — review, then Save.",
|
||||
"ja": "AI編集を適用 — 確認して保存してください。",
|
||||
"vi": "Đã áp dụng chỉnh sửa của AI — xem lại rồi Lưu."},
|
||||
"fileedit.ai_empty": {"en": "The AI returned no content.", "ja": "AIが内容を返しませんでした。",
|
||||
"vi": "AI không trả về nội dung."},
|
||||
"fileedit.ai_failed": {"en": "AI edit failed: {err}", "ja": "AI編集に失敗: {err}",
|
||||
"vi": "Sửa bằng AI thất bại: {err}"},
|
||||
"fileedit.saved": {"en": "Saved {path} (original backed up as .bak).",
|
||||
"ja": "{path} を保存(元は .bak にバックアップ)。",
|
||||
"vi": "Đã lưu {path} (bản gốc sao lưu thành .bak)."},
|
||||
}
|
||||
@@ -0,0 +1,342 @@
|
||||
"""Chuỗi hiển thị — phần skills_dialog.
|
||||
|
||||
Cắt ra từ ``i18n.py``: một dict 3.000 dòng không mở nổi để sửa một chữ.
|
||||
Cắt theo mốc phân đoạn có sẵn, nên mỗi cụm vẫn là các màn đi liền nhau.
|
||||
``i18n.py`` gộp tất cả lại thành ``STRINGS``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Dict
|
||||
|
||||
STRINGS: Dict[str, Dict[str, str]] = {
|
||||
"settings.testing_connection": {"en": "Testing connection…", "ja": "接続を確認中…", "vi": "Đang kiểm tra kết nối…"},
|
||||
"settings.sending_test": {"en": "Sending test…", "ja": "テスト送信中…", "vi": "Đang gửi thử…"},
|
||||
"settings.test_failed": {"en": "Test failed: {err}", "ja": "テスト失敗: {err}", "vi": "Kiểm tra thất bại: {err}"},
|
||||
"settings.pick_hist_dir": {"en": "Choose history folder", "ja": "履歴フォルダを選択", "vi": "Chọn thư mục lưu lịch sử"},
|
||||
|
||||
# ---- skills_dialog.py -----------------------------------------------
|
||||
"skills.edit_title": {"en": "Edit skill", "ja": "スキルを編集", "vi": "Sửa skill"},
|
||||
"skills.add_title": {"en": "Add skill", "ja": "スキルを追加", "vi": "Thêm skill"},
|
||||
"skills.name_label": {"en": "Skill name", "ja": "スキル名", "vi": "Tên skill"},
|
||||
"skills.name_placeholder": {
|
||||
"en": "e.g. Always write unit tests", "ja": "例:常に単体テストを書く", "vi": "vd. Luôn viết unit test"},
|
||||
"skills.desc_label": {"en": "Short description (optional)", "ja": "簡単な説明(任意)", "vi": "Mô tả ngắn (tuỳ chọn)"},
|
||||
"skills.instructions_label": {"en": "Instructions for the agent", "ja": "エージェントへの指示", "vi": "Hướng dẫn cho agent"},
|
||||
"skills.gen_from_desc": {"en": "Generate from description", "ja": "説明文から生成", "vi": "Tạo từ mô tả"},
|
||||
"skills.gen_from_desc_tooltip": {
|
||||
"en": "Use the AI agent to draft the instructions from the short description",
|
||||
"ja": "AI エージェントで短い説明から指示文の下書きを生成します",
|
||||
"vi": "Dùng AI để soạn hướng dẫn từ mô tả ngắn"},
|
||||
"skills.instructions_placeholder": {
|
||||
"en": "Describe the rules / guidance the agent must follow…",
|
||||
"ja": "エージェントが従うべきルール/ガイドラインを記述…",
|
||||
"vi": "Mô tả các quy tắc/hướng dẫn mà agent phải tuân theo…"},
|
||||
"skills.generating": {"en": "Generating…", "ja": "生成中…", "vi": "Đang tạo…"},
|
||||
"skills.title": {"en": "Skills", "ja": "スキル", "vi": "Skills"},
|
||||
"skills.hint": {
|
||||
"en": "Tick to enable a skill. Enabled skills are followed by the agent.",
|
||||
"ja": "チェックでスキルを有効化。有効なスキルはエージェントが従います。",
|
||||
"vi": "Tick để bật skill. Skill đang bật sẽ được agent tuân theo."},
|
||||
"skills.auto_generate": {"en": "Auto-generate", "ja": "自動生成", "vi": "Tự động tạo"},
|
||||
"skills.auto_generate_tooltip": {
|
||||
"en": ("Describe a skill in one line and let the AI draft the whole skill "
|
||||
"(name, description and instructions) for you to review."),
|
||||
"ja": "1行でスキルを説明すると、AI が名前・説明・指示文をまとめて下書きします。",
|
||||
"vi": "Mô tả skill trong 1 dòng, AI sẽ tự soạn cả skill (tên, mô tả, hướng dẫn) để bạn xem lại."},
|
||||
"skills.import_btn": {"en": "Import…", "ja": "インポート…", "vi": "Nhập…"},
|
||||
"skills.import_tooltip": {
|
||||
"en": "Import an external skill from a .skill, .json, .md or .txt file",
|
||||
"ja": ".skill / .json / .md / .txt ファイルから外部スキルをインポート",
|
||||
"vi": "Nhập skill từ file .skill, .json, .md hoặc .txt"},
|
||||
"skills.edit_btn": {"en": "Edit", "ja": "編集", "vi": "Sửa"},
|
||||
"skills.delete_btn": {"en": "Delete", "ja": "削除", "vi": "Xóa"},
|
||||
"skills.close_btn": {"en": "Close", "ja": "閉じる", "vi": "Đóng"},
|
||||
"skills.no_skills": {
|
||||
"en": "(No skills yet — click ' Auto-generate' or 'Import…')",
|
||||
"ja": "(スキルはまだありません。「 自動生成」または「インポート…」をクリック)",
|
||||
"vi": "(Chưa có skill nào — bấm ' Tự động tạo' hoặc 'Nhập…')"},
|
||||
"skills.auto_generate_title": {"en": "Auto-generate skill", "ja": "スキルを自動生成", "vi": "Tự động tạo skill"},
|
||||
"skills.auto_generate_unavailable": {
|
||||
"en": "AI generation isn't available right now.", "ja": "現在 AI 生成は利用できません。",
|
||||
"vi": "Tính năng tạo bằng AI hiện chưa dùng được."},
|
||||
"skills.auto_generate_prompt": {
|
||||
"en": "Describe the skill you want (what should the agent do?):",
|
||||
"ja": "欲しいスキルを説明してください(エージェントに何をさせたいか):",
|
||||
"vi": "Mô tả skill bạn muốn (agent nên làm gì?):"},
|
||||
"skills.auto_generate_failed": {
|
||||
"en": "Couldn't generate a skill. Check the AI provider in Settings, or add one manually.",
|
||||
"ja": "スキルを生成できませんでした。設定の AI プロバイダーを確認するか、手動で追加してください。",
|
||||
"vi": "Không tạo được skill. Kiểm tra lại provider AI trong Settings, hoặc tự thêm thủ công."},
|
||||
"skills.import_dialog_title": {"en": "Import skill", "ja": "スキルをインポート", "vi": "Nhập skill"},
|
||||
"skills.import_dialog_filter": {
|
||||
"en": "Skills (*.skill *.json *.md *.txt *.yaml *.yml *.zip);;All files (*.*)",
|
||||
"ja": "スキル (*.skill *.json *.md *.txt *.yaml *.yml *.zip);;すべてのファイル (*.*)",
|
||||
"vi": "Skill (*.skill *.json *.md *.txt *.yaml *.yml *.zip);;Tất cả file (*.*)"},
|
||||
"skills.import_failed": {"en": "Could not import: {err}", "ja": "インポートできませんでした: {err}", "vi": "Không nhập được: {err}"},
|
||||
"skills.export_btn": {"en": "Export .md", "ja": ".md エクスポート", "vi": "Xuất .md"},
|
||||
"skills.export_tooltip": {
|
||||
"en": "Export the selected skill to a Markdown (.md) file",
|
||||
"ja": "選択したスキルを Markdown (.md) ファイルに書き出します",
|
||||
"vi": "Xuất skill đang chọn ra file Markdown (.md)"},
|
||||
"skills.export_pick": {
|
||||
"en": "Select a skill in the list first, then click Export .md.",
|
||||
"ja": "先にリストでスキルを選択してから「.md エクスポート」を押してください。",
|
||||
"vi": "Hãy chọn một skill trong danh sách trước, rồi bấm Xuất .md."},
|
||||
"skills.export_dialog_title": {
|
||||
"en": "Export skill to Markdown", "ja": "スキルを Markdown に書き出す",
|
||||
"vi": "Xuất skill ra Markdown"},
|
||||
"skills.export_dialog_filter": {
|
||||
"en": "Markdown (*.md);;All files (*.*)", "ja": "Markdown (*.md);;すべてのファイル (*.*)",
|
||||
"vi": "Markdown (*.md);;Tất cả file (*.*)"},
|
||||
"skills.export_done": {
|
||||
"en": "Exported to {path}", "ja": "{path} に書き出しました", "vi": "Đã xuất ra {path}"},
|
||||
"skills.export_failed": {
|
||||
"en": "Could not export: {err}", "ja": "書き出せませんでした: {err}", "vi": "Không xuất được: {err}"},
|
||||
"skills.duplicate_btn": {"en": "Duplicate", "ja": "複製", "vi": "Nhân bản"},
|
||||
"skills.duplicate_tooltip": {
|
||||
"en": "Duplicate the selected skill (a copy you can rename and edit)",
|
||||
"ja": "選択したスキルを複製します(名前を変更・編集できるコピー)",
|
||||
"vi": "Nhân bản skill đang chọn (bản sao có thể đổi tên và chỉnh sửa)"},
|
||||
"skills.copy_name": {"en": "{name} (copy)", "ja": "{name}(コピー)", "vi": "{name} (bản sao)"},
|
||||
"skills.from_template": {"en": "From template file…", "ja": "テンプレートファイルから…", "vi": "Từ file template…"},
|
||||
"skills.from_template_tooltip": {
|
||||
"en": ("Analyze a .pptx/.xlsx template's layout, fonts, colors and formatting "
|
||||
"and draft a skill so future generated files match it."),
|
||||
"ja": ".pptx/.xlsx テンプレートのレイアウト・フォント・色・書式を解析し、"
|
||||
"今後生成するファイルがそれに合うようスキルを下書きします。",
|
||||
"vi": "Phân tích layout/font/màu/định dạng của file template .pptx/.xlsx, soạn skill để các file tạo sau khớp với nó."},
|
||||
"skills.from_template_title": {
|
||||
"en": "Generate skill from template", "ja": "テンプレートからスキルを生成",
|
||||
"vi": "Tạo skill từ template"},
|
||||
"skills.from_template_dialog_title": {
|
||||
"en": "Select a template file", "ja": "テンプレートファイルを選択",
|
||||
"vi": "Chọn file template"},
|
||||
"skills.from_template_dialog_filter": {
|
||||
"en": "PowerPoint/Excel (*.pptx *.xlsx *.xlsm);;All files (*.*)",
|
||||
"ja": "PowerPoint/Excel (*.pptx *.xlsx *.xlsm);;すべてのファイル (*.*)",
|
||||
"vi": "PowerPoint/Excel (*.pptx *.xlsx *.xlsm);;Tất cả file (*.*)"},
|
||||
"skills.from_template_failed": {
|
||||
"en": ("Couldn't analyze this template. Make sure it's a valid .pptx/.xlsx file "
|
||||
"and the AI provider in Settings works, or add the skill manually."),
|
||||
"ja": "このテンプレートを解析できませんでした。有効な .pptx/.xlsx ファイルか、"
|
||||
"設定の AI プロバイダーが動作しているか確認するか、手動でスキルを追加してください。",
|
||||
"vi": "Không phân tích được template này. Kiểm tra file .pptx/.xlsx hợp lệ và provider AI trong Settings hoạt động tốt, hoặc tự thêm skill thủ công."},
|
||||
|
||||
# ---- flow_dialog.py -----------------------------------------------
|
||||
"flow.title": {"en": "Flow Management", "ja": "Flow Management", "vi": "Flow Management"},
|
||||
"flow.tab_flow": {"en": "Flow", "ja": "フロー", "vi": "Flow"},
|
||||
"flow.tab_agents": {"en": "Agents", "ja": "エージェント", "vi": "Agents"},
|
||||
"flow.tab_skills": {"en": "Skills", "ja": "スキル", "vi": "Skills"},
|
||||
"flow.template": {"en": "Template:", "ja": "テンプレート:", "vi": "Template:"},
|
||||
"flow.load_builtin": {"en": "Load Req→Demo template", "ja": "Req→Demo テンプレートを読込", "vi": "Tải template Req→Demo"},
|
||||
"flow.new": {"en": "New", "ja": "新規", "vi": "Mới"},
|
||||
"flow.delete_template": {"en": "Delete template", "ja": "テンプレートを削除", "vi": "Xóa template"},
|
||||
"flow.name_label": {"en": "Flow name", "ja": "フロー名", "vi": "Tên flow"},
|
||||
"flow.description_label": {"en": "Description", "ja": "説明", "vi": "Mô tả"},
|
||||
"flow.stages": {"en": "Stages", "ja": "ステージ", "vi": "Các bước"},
|
||||
"flow.remove_stage": {"en": "Remove stage", "ja": "ステージを削除", "vi": "Xóa bước"},
|
||||
"flow.stage_name": {"en": "Stage name", "ja": "ステージ名", "vi": "Tên bước"},
|
||||
"flow.hint": {"en": "Hint", "ja": "ヒント", "vi": "Gợi ý"},
|
||||
"flow.task_prompt": {"en": "Task (prompt)", "ja": "タスク(プロンプト)", "vi": "Nhiệm vụ (prompt)"},
|
||||
"flow.skill": {"en": "Skill", "ja": "スキル", "vi": "Skill"},
|
||||
"flow.agent": {"en": "AI provider", "ja": "AI プロバイダー", "vi": "AI provider"},
|
||||
"flow.model_label": {"en": "Agent:", "ja": "Agent:", "vi": "Agent:"},
|
||||
"flow.default_model": {"en": "(provider default)", "ja": "(プロバイダー既定)", "vi": "(mặc định của provider)"},
|
||||
"flow.gen_task_from_hint": {"en": "Generate task from hint", "ja": "ヒントからタスクを生成", "vi": "Tạo task từ gợi ý"},
|
||||
"flow.gen_task_tooltip": {
|
||||
"en": "Use the AI agent to expand the hint into a task prompt",
|
||||
"ja": "AI エージェントでヒントをタスクプロンプトに展開します",
|
||||
"vi": "Dùng AI để mở rộng gợi ý thành task prompt"},
|
||||
"flow.attachments": {"en": "Attachments", "ja": "添付ファイル", "vi": "Đính kèm"},
|
||||
"flow.attach_files": {"en": "Attach files…", "ja": "ファイルを添付…", "vi": "Đính kèm file…"},
|
||||
"flow.attach_files_count": {
|
||||
"en": "{n} file(s) attached", "ja": "{n} 件添付済み", "vi": "Đã đính kèm {n} file"},
|
||||
"flow.compact_after_run": {
|
||||
"en": "Compact after run", "ja": "実行後に圧縮", "vi": "Compact after run (nén sau khi chạy)"},
|
||||
"flow.compact_after_run_tooltip": {
|
||||
"en": "Trim older history right after this stage, freeing up token space for the next one",
|
||||
"ja": "このステージの直後に古い履歴を切り詰め、次のステージ用にトークン余裕を確保します",
|
||||
"vi": "Rút gọn lịch sử cũ ngay sau bước này để nhường chỗ token cho bước tiếp theo"},
|
||||
"flow.self_verify": {
|
||||
"en": "Self-verify before handoff", "ja": "引き渡し前に自己検証", "vi": "Self-verify trước khi bàn giao"},
|
||||
"flow.self_verify_tooltip": {
|
||||
"en": "Ask the agent to confirm the stage is actually complete before moving on",
|
||||
"ja": "次に進む前に、このステージが本当に完了しているかエージェントに確認させます",
|
||||
"vi": "Yêu cầu agent tự xác nhận đã hoàn thành đầy đủ trước khi qua bước sau"},
|
||||
"flow.review_retries": {
|
||||
"en": "Review-completeness retries", "ja": "完全性レビューの再試行回数", "vi": "Số lần review lại nếu chưa xong"},
|
||||
"flow.review_retries_tooltip": {
|
||||
"en": "If the self-check says the stage is incomplete, re-run it up to this many times (0 = off)",
|
||||
"ja": "自己チェックで未完了と判定された場合、この回数まで再実行します(0 = 無効)",
|
||||
"vi": "Nếu tự kiểm tra thấy chưa hoàn thành, chạy lại bước này tối đa số lần này (0 = tắt)"},
|
||||
"flow.parallel_agents": {
|
||||
"en": "Parallel sub-agents", "ja": "並列サブエージェント", "vi": "Sub-agent chạy song song"},
|
||||
"flow.subagent_name_placeholder": {"en": "Name (e.g. backend)", "ja": "名前(例: backend)", "vi": "Tên (vd backend)"},
|
||||
"flow.subagent_task_placeholder": {
|
||||
"en": "Task for this sub-agent (optional — falls back to the stage task)",
|
||||
"ja": "このサブエージェントのタスク(任意 — 未入力ならステージのタスクを使用)",
|
||||
"vi": "Nhiệm vụ của sub-agent này (tùy chọn — bỏ trống thì dùng task của bước)"},
|
||||
"flow.subagent_add": {"en": "Add", "ja": "追加", "vi": "Thêm"},
|
||||
"flow.subagent_remove": {"en": "Remove", "ja": "削除", "vi": "Xóa"},
|
||||
"flow.subagent_add_from_agent": {
|
||||
"en": "Add from Agent", "ja": "エージェントから追加", "vi": "Thêm từ Agent"},
|
||||
"flow.subagent_no_agents": {
|
||||
"en": "(no saved Agents — create one in the Agents tab)",
|
||||
"ja": "(保存済みのエージェントがありません — Agents タブで作成してください)",
|
||||
"vi": "(chưa có Agent nào — tạo ở tab Quản lý Agent)"},
|
||||
"flow.subagent_hint": {
|
||||
"en": ("Add 2+ sub-agents to make this a PARALLEL stage — they run concurrently, then "
|
||||
"the stage's own Task field is used to consolidate their results into one."),
|
||||
"ja": ("サブエージェントを2つ以上追加すると、このステージは並列ステージになります — "
|
||||
"同時に実行され、その後ステージ自体のタスク欄で結果を1つに統合します。"),
|
||||
"vi": ("Thêm từ 2 sub-agent trở lên để bước này chạy SONG SONG — chúng chạy đồng thời, "
|
||||
"sau đó ô Task của chính bước này dùng để gộp kết quả lại thành một.")},
|
||||
"flow.add_stage": {"en": "Add stage", "ja": "ステージを追加", "vi": "Thêm bước"},
|
||||
"flow.update_stage": {"en": "Update stage", "ja": "ステージを更新", "vi": "Cập nhật bước"},
|
||||
"flow.save_template": {"en": "Save as template", "ja": "テンプレートとして保存", "vi": "Lưu làm template"},
|
||||
"flow.run": {"en": "Run flow", "ja": "フローを実行", "vi": "Chạy flow"},
|
||||
"flow.close": {"en": "Close", "ja": "閉じる", "vi": "Đóng"},
|
||||
"flow.none": {"en": "(none)", "ja": "(なし)", "vi": "(không có)"},
|
||||
"flow.default_agent": {"en": "Default", "ja": "デフォルト", "vi": "Mặc định"},
|
||||
"flow.select_template": {"en": "— select template —", "ja": "— テンプレートを選択 —", "vi": "— chọn template —"},
|
||||
"flow.new_flow_name": {"en": "New flow", "ja": "新しいフロー", "vi": "Flow mới"},
|
||||
"flow.default_name": {"en": "Flow", "ja": "フロー", "vi": "Flow"},
|
||||
|
||||
# ---- agent_manager_tab.py -------------------------------------------
|
||||
"agentmgr.hint": {
|
||||
"en": "Create reusable Agent presets (name + task + provider) — pick them as "
|
||||
"parallel sub-agents from any Flow stage in the Code tab.",
|
||||
"ja": "再利用できるエージェントのプリセット(名前・タスク・プロバイダー)を作成します — "
|
||||
"Code タブの任意のフローステージから並列サブエージェントとして選択できます。",
|
||||
"vi": "Tạo sẵn các Agent (tên + nhiệm vụ + provider) để tái sử dụng — chọn làm "
|
||||
"sub-agent chạy song song từ bất kỳ bước Flow nào ở tab Code."},
|
||||
"agentmgr.list_label": {"en": "Saved agents", "ja": "保存済みエージェント", "vi": "Agent đã lưu"},
|
||||
"agentmgr.name_label": {"en": "Agent name", "ja": "エージェント名", "vi": "Tên agent"},
|
||||
"agentmgr.desc_label": {"en": "Description", "ja": "説明", "vi": "Mô tả"},
|
||||
"agentmgr.prompt_label": {"en": "Task (prompt)", "ja": "タスク(プロンプト)", "vi": "Nhiệm vụ (prompt)"},
|
||||
"agentmgr.gen_prompt_btn": {"en": "Generate from description", "ja": "説明から生成",
|
||||
"vi": "Tạo prompt từ mô tả"},
|
||||
"agentmgr.gen_prompt_tooltip": {
|
||||
"en": "Use the AI agent to expand the name/description into a task prompt",
|
||||
"ja": "AI エージェントで名前・説明をタスクプロンプトに展開します",
|
||||
"vi": "Dùng AI để mở rộng tên/mô tả thành task prompt"},
|
||||
"agentmgr.provider_label": {"en": "AI provider", "ja": "AI プロバイダー", "vi": "Provider AI"},
|
||||
"agentmgr.new_btn": {"en": "New agent", "ja": "新規エージェント", "vi": "Agent mới"},
|
||||
"agentmgr.save_btn": {"en": "Save agent", "ja": "エージェントを保存", "vi": "Lưu agent"},
|
||||
"agentmgr.delete_btn": {"en": "Delete", "ja": "削除", "vi": "Xóa"},
|
||||
"agentmgr.delete_confirm": {
|
||||
"en": "Delete agent '{name}'?", "ja": "エージェント「{name}」を削除しますか?",
|
||||
"vi": "Xóa agent '{name}'?"},
|
||||
|
||||
# ---- permission_dialog.py ------------------------------------------
|
||||
"permission.title": {"en": "Confirm action", "ja": "操作を確認", "vi": "Xác nhận thao tác"},
|
||||
"permission.default_action": {"en": "Action", "ja": "操作", "vi": "Thao tác"},
|
||||
"permission.subtitle_command": {
|
||||
"en": "The agent wants to run this command in the working folder:",
|
||||
"ja": "エージェントが作業フォルダで次のコマンドを実行しようとしています:",
|
||||
"vi": "Agent muốn chạy lệnh này trong thư mục làm việc:"},
|
||||
"permission.subtitle_diff": {
|
||||
"en": "The agent wants to change a file (diff below):",
|
||||
"ja": "エージェントがファイルを変更しようとしています(差分は下記):",
|
||||
"vi": "Agent muốn thay đổi một tệp (xem diff bên dưới):"},
|
||||
"permission.subtitle_default": {
|
||||
"en": "The agent proposes an action:", "ja": "エージェントが操作を提案しています:",
|
||||
"vi": "Agent đề xuất một thao tác:"},
|
||||
"permission.approve": {"en": "Approve", "ja": "承認", "vi": "Duyệt"},
|
||||
"permission.reject": {"en": "Reject", "ja": "拒否", "vi": "Từ chối"},
|
||||
"permission.remember_whitelist": {
|
||||
"en": "Remember — add to the command whitelist",
|
||||
"ja": "記憶する — コマンドのホワイトリストに追加",
|
||||
"vi": "Ghi nhớ — thêm vào whitelist lệnh"},
|
||||
"permission.remember_whitelist_tooltip": {
|
||||
"en": "Future commands starting the same way will be auto-approved without asking again.",
|
||||
"ja": "同じように始まる今後のコマンドは、再確認なしで自動承認されます。",
|
||||
"vi": "Các lệnh sau bắt đầu giống vậy sẽ được tự động duyệt, không hỏi lại."},
|
||||
|
||||
|
||||
# ---- structure_graph_view.py ---------------------------------------
|
||||
"structure.path_placeholder": {"en": "Source / document folder", "ja": "ソース/ドキュメントフォルダ", "vi": "Thư mục source/tài liệu"},
|
||||
"structure.browse": {"en": "Browse…", "ja": "参照…", "vi": "Browse…"},
|
||||
"structure.mode_all": {"en": "All files", "ja": "すべてのファイル", "vi": "All files"},
|
||||
"structure.mode_code": {"en": "Code only", "ja": "コードのみ", "vi": "Code only"},
|
||||
"structure.mode_doc": {"en": "Docs only", "ja": "ドキュメントのみ", "vi": "Docs only"},
|
||||
"structure.project_none": {"en": "(no project — free path)", "ja": "(プロジェクトなし — 自由パス)", "vi": "(không gán project — path tự do)"},
|
||||
"structure.project_tooltip": {
|
||||
"en": "Lock the scan to a project's sandbox workspace — path becomes read-only and the "
|
||||
"Agent Q&A below follows that project's shared Instructions (safer, grounded answers).",
|
||||
"ja": "スキャン対象をプロジェクトのサンドボックスワークスペースに固定します — パスは読み取り専用になり、"
|
||||
"下のエージェントQ&Aはそのプロジェクトの共有指示に従います(より安全で根拠のある回答)。",
|
||||
"vi": "Khóa phạm vi quét vào đúng thư mục sandbox của 1 project — path chuyển sang chỉ đọc và "
|
||||
"khung hỏi-đáp Agent bên dưới sẽ theo Instructions chung của project đó (an toàn hơn, "
|
||||
"câu trả lời bám sát ngữ cảnh, giảm bịa đặt).",
|
||||
},
|
||||
"structure.scan": {"en": "Scan", "ja": "スキャン", "vi": "Scan"},
|
||||
"structure.export_png": {"en": "Export PNG", "ja": "PNG エクスポート", "vi": "Xuất PNG"},
|
||||
"structure.msgs_btn": {"en": "Messages", "ja": "メッセージ", "vi": "Tin nhắn"},
|
||||
"structure.graph_btn": {"en": "Graph", "ja": "グラフ", "vi": "Đồ thị"},
|
||||
"structure.msgs_tooltip": {
|
||||
"en": "Show all conversation messages grouped by day (as JSON).",
|
||||
"ja": "会話メッセージを日別にJSONで表示。",
|
||||
"vi": "Xem mọi message hội thoại nhóm theo ngày (dạng JSON)."},
|
||||
"structure.msgs_none": {"en": "No messages yet.", "ja": "メッセージがありません。",
|
||||
"vi": "Chưa có message nào."},
|
||||
"structure.open_browser": {"en": "Open in browser", "ja": "ブラウザで開く", "vi": "Mở trong trình duyệt"},
|
||||
"structure.open_browser_tooltip": {
|
||||
"en": "Open the full interactive D3 graph in your default browser (works in every build, including the standalone .exe)",
|
||||
"ja": "既定のブラウザでフル機能の D3 グラフを開きます(スタンドアロン .exe を含むすべてのビルドで利用可能)",
|
||||
"vi": "Mở đồ thị D3 đầy đủ tính năng trong trình duyệt mặc định (dùng được ở mọi bản build, kể cả file .exe độc lập)",
|
||||
},
|
||||
"structure.opened_browser": {
|
||||
"en": "D3 graph opened in your browser at {url}",
|
||||
"ja": "ブラウザで D3 グラフを開きました: {url}",
|
||||
"vi": "Đã mở đồ thị D3 trong trình duyệt tại {url}",
|
||||
},
|
||||
"structure.cmem_ui_open": {"en": "Codebase Memory UI", "ja": "Codebase Memory UI", "vi": "Codebase Memory UI"},
|
||||
"structure.cmem_ui_back": {"en": "Back to D3 view", "ja": "D3 表示に戻る", "vi": "Về đồ thị D3"},
|
||||
"structure.cmem_ui_tooltip": {
|
||||
"en": "Open codebase-memory-mcp's own graph UI (Graph/Projects/Control) for the current scan path.",
|
||||
"ja": "現在のスキャンパスに対して codebase-memory-mcp 独自のグラフ UI(Graph/Projects/Control)を開きます。",
|
||||
"vi": "Mở UI đồ thị riêng của codebase-memory-mcp (Graph/Projects/Control) cho đường dẫn đang quét."},
|
||||
"structure.cmem_ui_starting": {
|
||||
"en": "Starting codebase-memory-mcp UI…", "ja": "codebase-memory-mcp の UI を起動中…",
|
||||
"vi": "Đang khởi động UI của codebase-memory-mcp…"},
|
||||
"structure.cmem_ui_opened_embedded": {
|
||||
"en": "codebase-memory-mcp UI loaded.", "ja": "codebase-memory-mcp の UI を読み込みました。",
|
||||
"vi": "Đã tải UI của codebase-memory-mcp."},
|
||||
"structure.cmem_ui_opened_browser": {
|
||||
"en": "codebase-memory-mcp UI opened in your browser at {url}",
|
||||
"ja": "ブラウザで codebase-memory-mcp の UI を開きました: {url}",
|
||||
"vi": "Đã mở UI của codebase-memory-mcp trong trình duyệt tại {url}"},
|
||||
"structure.cmem_ui_not_built": {
|
||||
"en": "This codebase-memory-mcp build has no embedded UI. Install the "
|
||||
"'codebase-memory-mcp-ui' release asset from the project's GitHub "
|
||||
"releases to use this view. ({err})",
|
||||
"ja": "この codebase-memory-mcp ビルドには UI が組み込まれていません。このビューを使うには "
|
||||
"GitHub リリースから 'codebase-memory-mcp-ui' をインストールしてください。({err})",
|
||||
"vi": "Bản build codebase-memory-mcp này không có UI nhúng. Cần cài "
|
||||
"release asset 'codebase-memory-mcp-ui' từ trang GitHub Releases của "
|
||||
"dự án để dùng chức năng này. ({err})"},
|
||||
"structure.cmem_ui_failed": {
|
||||
"en": "Could not open codebase-memory-mcp UI: {err}",
|
||||
"ja": "codebase-memory-mcp の UI を開けませんでした: {err}",
|
||||
"vi": "Không mở được UI của codebase-memory-mcp: {err}"},
|
||||
"structure.collapse_agent_tooltip": {"en": "Collapse the Agent panel", "ja": "エージェントパネルを折りたたむ", "vi": "Thu gọn bảng Agent"},
|
||||
"structure.expand_agent_tooltip": {
|
||||
"en": "Click to expand the Agent panel", "ja": "クリックしてエージェントパネルを展開",
|
||||
"vi": "Bấm để mở lại bảng Agent"},
|
||||
"structure.agent_header": {"en": "Agent — ask about the graph", "ja": "エージェント ― グラフについて質問", "vi": "Agent — hỏi về đồ thị"},
|
||||
"structure.ask_placeholder": {
|
||||
"en": "e.g. what calls main? which files define classes?",
|
||||
"ja": "例: main を呼んでいるのは?クラスを定義しているファイルは?",
|
||||
"vi": "vd. cái gì gọi hàm main? file nào định nghĩa class?"},
|
||||
"structure.ask": {"en": "Ask", "ja": "質問", "vi": "Hỏi"},
|
||||
"structure.detail_placeholder": {
|
||||
"en": "Click a node to open its folder, or ask the agent about the graph.",
|
||||
"ja": "ノードをクリックするとフォルダを開きます。またはエージェントにグラフについて質問できます。",
|
||||
"vi": "Nhấp node để mở thư mục, hoặc hỏi agent về đồ thị."},
|
||||
"structure.pick_folder_title": {"en": "Choose folder", "ja": "フォルダを選択", "vi": "Chọn thư mục"},
|
||||
"structure.scanning": {"en": "Scanning structure…", "ja": "構造をスキャン中…", "vi": "Đang quét cấu trúc…"},
|
||||
"structure.scan_error": {"en": "Scan error: {err}", "ja": "スキャンエラー: {err}", "vi": "Lỗi khi quét: {err}"},
|
||||
"structure.graph_summary": {"en": "Graph: {nodes} nodes, {edges} edges.{note}", "ja": "グラフ: ノード {nodes} 個、エッジ {edges} 個。{note}", "vi": "Đồ thị: {nodes} node, {edges} cạnh.{note}"},
|
||||
"structure.truncated_note": {"en": " (truncated — too many nodes)", "ja": " (切り捨て:ノードが多すぎます)", "vi": " (đã cắt bớt — quá nhiều node)"},
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
"""infrastructure/ — Chạm thế giới thật: file, keyring, HTTP, tiến trình. Cài đặt interface."""
|
||||
@@ -0,0 +1 @@
|
||||
"""Infrastructure config package: ConfigRepository and typed settings facades."""
|
||||
@@ -0,0 +1,104 @@
|
||||
"""Cấu hình ứng dụng — interface, chưa phải cài đặt.
|
||||
|
||||
Hợp đồng số 2 của mục chung. Đây là thứ gỡ chốt lớn nhất: **156 lời gọi
|
||||
``ctx.config.*`` nằm rải trong 29 file**, nên nếu N2 và N3 phải đợi
|
||||
``ConfigRepository`` bản thật (R02-T02, hạn 23/08) thì hai người mất mấy ngày
|
||||
đầu ngồi không.
|
||||
|
||||
Danh sách thuộc tính dưới đây không bịa ra: đếm trực tiếp chỗ đang gọi trong
|
||||
``core/``, ``ui/``, ``providers/`` và ``app.py`` rồi lấy những cái được dùng
|
||||
thật, xếp theo số lần gọi.
|
||||
|
||||
Một chỗ cố ý KHÔNG đưa vào: ``config.data`` (36 lần gọi, nhiều nhất). Đó là
|
||||
đống dict thô — cho nó vào interface là bê nguyên vấn đề cũ sang kiến trúc mới.
|
||||
Ai đang cần ``data`` thì mở issue để bổ sung một thuộc tính có kiểu rõ ràng.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, Protocol, runtime_checkable
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class ConfigRepository(Protocol):
|
||||
"""Đọc/ghi cấu hình. Cài đặt thật dùng ``AtomicJsonFile`` (R02-T01/T02)."""
|
||||
|
||||
# ---- provider ------------------------------------------------------
|
||||
@property
|
||||
def active_provider(self) -> str:
|
||||
"""Tên provider đang chọn (24 lời gọi)."""
|
||||
...
|
||||
|
||||
def set_active_provider(self, name: str) -> None:
|
||||
...
|
||||
|
||||
def provider_conf(self, name: str | None = None) -> Dict[str, Any]:
|
||||
"""Cấu hình của một provider (9 lời gọi).
|
||||
|
||||
CHÚ Ý — điểm còn bỏ ngỏ, xem ``docs/refactor/GammaTeam_decisions.md``:
|
||||
dict này còn chứa ``api_key`` hay không là quyết định chưa chốt. Có 5
|
||||
nơi đang đọc trực tiếp, 3 trong số đó thuộc ``providers/`` của Team Duy.
|
||||
"""
|
||||
...
|
||||
|
||||
# ---- đường dẫn -----------------------------------------------------
|
||||
@property
|
||||
def shared_dir(self) -> str:
|
||||
"""Thư mục dùng chung cho telemetry nhiều máy (10 lời gọi)."""
|
||||
...
|
||||
|
||||
def history_dir(self) -> Path:
|
||||
"""Thư mục lịch sử chat của project đang chọn (7 lời gọi)."""
|
||||
...
|
||||
|
||||
def cowork_output_dir(self) -> Path:
|
||||
"""Thư mục Cowork ghi kết quả ra (6 lời gọi)."""
|
||||
...
|
||||
|
||||
# ---- giao diện -----------------------------------------------------
|
||||
@property
|
||||
def theme(self) -> str:
|
||||
"""``"dark"`` | ``"light"`` | ``"system"`` (8 lời gọi)."""
|
||||
...
|
||||
|
||||
def set_theme(self, value: str) -> None:
|
||||
...
|
||||
|
||||
@property
|
||||
def language(self) -> str:
|
||||
"""``"vi"`` | ``"en"`` | ``"ja"`` (4 lời gọi)."""
|
||||
...
|
||||
|
||||
def set_language(self, value: str) -> None:
|
||||
...
|
||||
|
||||
# ---- các nhóm cấu hình còn lại -------------------------------------
|
||||
@property
|
||||
def routing(self) -> Dict[str, Any]:
|
||||
"""Cấu hình định tuyến model (7 lời gọi)."""
|
||||
...
|
||||
|
||||
@property
|
||||
def auth(self) -> Dict[str, Any]:
|
||||
"""Cấu hình đăng nhập (6 lời gọi)."""
|
||||
...
|
||||
|
||||
@property
|
||||
def agent_security(self) -> Dict[str, Any]:
|
||||
"""Chính sách an toàn cho agent (5 lời gọi)."""
|
||||
...
|
||||
|
||||
@property
|
||||
def tools_disabled(self) -> list[str]:
|
||||
"""Tool bị tắt (2 lời gọi)."""
|
||||
...
|
||||
|
||||
def set_tool_enabled(self, name: str, enabled: bool) -> None:
|
||||
...
|
||||
|
||||
# ---- ghi ------------------------------------------------------------
|
||||
def save(self) -> None:
|
||||
"""Ghi xuống đĩa. Bản thật ghi atomic — tạm + fsync + thay thế —
|
||||
nên tắt máy giữa chừng không làm hỏng file (R02-T01).
|
||||
"""
|
||||
...
|
||||
@@ -0,0 +1,388 @@
|
||||
"""ConfigRepository chạy trên file JSON — R02-T02.
|
||||
|
||||
Thay cho ``config.py::AppConfig``. Hai khác biệt duy nhất về hành vi, cả hai
|
||||
đều là thứ ta muốn:
|
||||
|
||||
1. Ghi qua :class:`AtomicJsonFile` — mất điện giữa lúc lưu không còn làm hỏng
|
||||
cấu hình (R02-T01).
|
||||
2. API key đọc từ :class:`SecretStore` rồi **ghép vào** dict do
|
||||
``provider_conf()`` trả về — đúng đường A đã chốt 21/08
|
||||
(``docs/refactor/GammaTeam_decisions.md``). Nhờ vậy 5 nơi đang đọc
|
||||
``conf["api_key"]`` không phải sửa dòng nào, trong đó 3 nơi thuộc Team Duy.
|
||||
|
||||
Mọi thứ còn lại giữ nguyên có chủ đích: trộn sâu với mặc định, đọc biến môi
|
||||
trường, ``ms365.unlocked`` không bao giờ chạm đĩa. Đây là refactor — hành vi
|
||||
nhìn từ ngoài phải y hệt.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
from ..persistence.json.atomic_json_file import AtomicJsonFile
|
||||
from ..secrets.secret_store import SecretStore, provider_key
|
||||
from .schema_migration import CURRENT_VERSION, migrate
|
||||
|
||||
|
||||
class JsonConfigRepository:
|
||||
"""Cấu hình đọc/ghi từ một file JSON, bí mật để trong ``SecretStore``.
|
||||
|
||||
``secrets`` để None nghĩa là không có kho bí mật — mọi thứ vẫn chạy, chỉ
|
||||
là ``api_key`` lấy nguyên từ file như trước. Cần vậy để chuyển dần
|
||||
(R02-T05) chứ không phải đổi một phát cả app.
|
||||
"""
|
||||
|
||||
def __init__(self, path: Path, *, secrets: SecretStore | None = None,
|
||||
defaults: Dict[str, Any] | None = None,
|
||||
env_overrides=None):
|
||||
self._file = AtomicJsonFile(path)
|
||||
self._secrets = secrets
|
||||
# Lấy thẳng từ config.py để hai bên không lệch nhau trong lúc chuyển.
|
||||
if defaults is None or env_overrides is None:
|
||||
from ... import config as legacy
|
||||
defaults = defaults if defaults is not None else legacy.DEFAULT_CONFIG
|
||||
env_overrides = env_overrides or legacy._apply_env_overrides
|
||||
self._defaults = defaults
|
||||
self._env_overrides = env_overrides
|
||||
self.data: Dict[str, Any] = self._load()
|
||||
|
||||
@classmethod
|
||||
def from_data(cls, data: Dict[str, Any], path: Path):
|
||||
"""Dựng từ dict có sẵn — KHÔNG đọc đĩa, KHÔNG nâng cấp schema.
|
||||
|
||||
Dành cho test: chúng dựng cấu hình trong bộ nhớ rồi mới ghi. Đi qua
|
||||
``__init__`` thường thì nó đọc file (chưa có) và có thể chạy migration
|
||||
trên dữ liệu test, tức là test đo nhầm thứ khác.
|
||||
"""
|
||||
obj = cls.__new__(cls)
|
||||
obj._file = AtomicJsonFile(Path(path))
|
||||
obj._secrets = None
|
||||
from ... import config as legacy
|
||||
obj._defaults = legacy.DEFAULT_CONFIG
|
||||
obj._env_overrides = legacy._apply_env_overrides
|
||||
obj.data = data
|
||||
return obj
|
||||
|
||||
# ---- nạp ------------------------------------------------------------
|
||||
def _load(self) -> Dict[str, Any]:
|
||||
merged = copy.deepcopy(self._defaults)
|
||||
stored = self._file.read(default=None)
|
||||
if isinstance(stored, dict):
|
||||
# Nâng cấp TRƯỚC khi trộn với mặc định: bước v1→v2 gỡ api_key khỏi
|
||||
# đĩa, mà mặc định thì không có khoá nào để gỡ.
|
||||
stored, changed = migrate(stored, secrets=self._secrets,
|
||||
path=self._file.path)
|
||||
merged = _deep_merge(merged, stored)
|
||||
if changed:
|
||||
self.data = merged
|
||||
self.save() # ghi ngay, để lần sau khỏi chuyển lại
|
||||
merged = self._env_overrides(merged)
|
||||
# Trạng thái mở khoá ms365 chỉ tồn tại lúc chạy — mỗi lần mở app đều
|
||||
# bắt đầu ở trạng thái khoá, không tin giá trị đọc từ đĩa.
|
||||
merged.setdefault("ms365", {})["unlocked"] = False
|
||||
return merged
|
||||
|
||||
def reload(self) -> None:
|
||||
self.data = self._load()
|
||||
|
||||
# ---- provider --------------------------------------------------------
|
||||
@property
|
||||
def active_provider(self) -> str:
|
||||
return self.data.get("active_provider", "")
|
||||
|
||||
@active_provider.setter
|
||||
def active_provider(self, name: str) -> None:
|
||||
"""``AppConfig`` cũ cho gán thẳng, và 3 chỗ trong app.py đang gán. Bỏ
|
||||
setter đi thì Qt nuốt AttributeError trong slot và triệu chứng là
|
||||
"bấm không ăn", không có lỗi nào hiện ra — mất hẳn một buổi mới truy
|
||||
ra. Refactor thì hành vi nhìn từ ngoài phải y hệt."""
|
||||
self.data["active_provider"] = name
|
||||
|
||||
def set_active_provider(self, name: str) -> None:
|
||||
self.data["active_provider"] = name
|
||||
|
||||
def provider_conf(self, name: str | None = None) -> Dict[str, Any]:
|
||||
"""Cấu hình provider, có sẵn ``api_key``.
|
||||
|
||||
Trả về BẢN SAO: chỗ gọi sửa dict này thì không được âm thầm ghi ngược
|
||||
vào cấu hình — và quan trọng hơn, khoá vừa ghép vào không được lẫn
|
||||
ngược vào ``self.data`` rồi theo ``save()`` xuống đĩa.
|
||||
"""
|
||||
name = name or self.active_provider
|
||||
conf = dict(self.data.get("providers", {}).get(name, {}))
|
||||
if self._secrets is not None:
|
||||
stored = self._secrets.get(provider_key(name))
|
||||
if stored:
|
||||
conf["api_key"] = stored
|
||||
return conf
|
||||
|
||||
def set_api_key(self, name: str, value: str) -> None:
|
||||
"""Lưu khoá vào kho bí mật, và xoá khỏi cấu hình trên đĩa.
|
||||
|
||||
Đây là nửa còn lại của đường A: dict *đọc ra* vẫn có ``api_key``,
|
||||
nhưng file JSON *trên đĩa* thì không — điều kiện để qua CASAN Check 1.
|
||||
"""
|
||||
if self._secrets is not None:
|
||||
self._secrets.set(provider_key(name), value)
|
||||
self.data.setdefault("providers", {}).setdefault(name, {})["api_key"] = ""
|
||||
else:
|
||||
self.data.setdefault("providers", {}).setdefault(name, {})["api_key"] = value
|
||||
|
||||
# ---- đường dẫn -------------------------------------------------------
|
||||
@property
|
||||
def shared_dir(self) -> str:
|
||||
return self.data.get("shared_dir", "")
|
||||
|
||||
def history_dir(self) -> Path:
|
||||
rt = self.data.get("_project_history_dir")
|
||||
if rt:
|
||||
return Path(rt)
|
||||
custom = (self.data.get("history", {}).get("custom_dir") or "").strip()
|
||||
if custom:
|
||||
return Path(custom).expanduser()
|
||||
from ...config import CONFIG_DIR
|
||||
return CONFIG_DIR / "history"
|
||||
|
||||
def cowork_output_dir(self) -> Path:
|
||||
custom = (self.data.get("cowork", {}).get("output_dir") or "").strip()
|
||||
if custom:
|
||||
return Path(custom).expanduser()
|
||||
from ... import paths
|
||||
from ...config import CONFIG_DIR
|
||||
root = paths.primary_onedrive_root()
|
||||
if root is not None:
|
||||
return root / "CoworkLocal" / "output"
|
||||
return CONFIG_DIR / "output" / "cowork"
|
||||
|
||||
# ---- giao diện -------------------------------------------------------
|
||||
@property
|
||||
def theme(self) -> str:
|
||||
return self.data.get("theme", "dark")
|
||||
|
||||
@theme.setter
|
||||
def theme(self, value: str) -> None:
|
||||
self.data["theme"] = value
|
||||
|
||||
def set_theme(self, value: str) -> None:
|
||||
self.data["theme"] = value
|
||||
|
||||
@property
|
||||
def language(self) -> str:
|
||||
return self.data.get("language", "vi")
|
||||
|
||||
@language.setter
|
||||
def language(self, value: str) -> None:
|
||||
self.data["language"] = value
|
||||
|
||||
def set_language(self, value: str) -> None:
|
||||
self.data["language"] = value
|
||||
|
||||
# ---- nhóm cấu hình ---------------------------------------------------
|
||||
@property
|
||||
def routing(self) -> Dict[str, Any]:
|
||||
return self.data.setdefault("routing", {})
|
||||
|
||||
@property
|
||||
def auth(self) -> Dict[str, Any]:
|
||||
return self.data.setdefault("auth", {})
|
||||
|
||||
@property
|
||||
def agent_security(self) -> Dict[str, Any]:
|
||||
return self.data.setdefault("agent_security", {})
|
||||
|
||||
@property
|
||||
def tools_disabled(self) -> list[str]:
|
||||
return list(self.data.get("tools_disabled", []))
|
||||
|
||||
def set_tool_enabled(self, name: str, enabled: bool) -> None:
|
||||
disabled = list(self.data.get("tools_disabled", []))
|
||||
if enabled:
|
||||
disabled = [t for t in disabled if t != name]
|
||||
elif name not in disabled:
|
||||
disabled.append(name)
|
||||
self.data["tools_disabled"] = disabled
|
||||
|
||||
|
||||
# ---- phần bù để thay được AppConfig ----------------------------------
|
||||
# 21 thành viên dưới đây chép nguyên ngữ nghĩa từ ``config.py::AppConfig``.
|
||||
# Không phải thiết kế mới: chừng nào 29 file còn gọi qua ``ctx.config`` thì
|
||||
# repository phải trả lời được đúng những câu hỏi cũ, nếu không thì không
|
||||
# tráo được. Dọn lại là việc của các R sau, không phải của R02.
|
||||
|
||||
#: Các chế độ định tuyến. Delta thêm "fallback" ở R03-T03. Định nghĩa ở đây
|
||||
#: là bản chính; ``tests/test_config_repository.py`` có bài đối chiếu với
|
||||
#: ``config.py`` để hai bên lệch nhau là đỏ ngay.
|
||||
ROUTING_MODES = ("off", "auto", "manual", "fallback")
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: Path | None = None, *, secrets: SecretStore | None = None):
|
||||
"""Dựng repository từ đường dẫn mặc định — thay ``AppConfig.load()``."""
|
||||
if path is None:
|
||||
from ... import config as legacy
|
||||
path = legacy.CONFIG_PATH
|
||||
return cls(Path(path), secrets=secrets)
|
||||
|
||||
@property
|
||||
def path(self) -> Path:
|
||||
return self._file.path
|
||||
|
||||
# ---- TLS -------------------------------------------------------------
|
||||
|
||||
@property
|
||||
def ca_bundle(self) -> str:
|
||||
"""Đường dẫn file PEM riêng, hoặc '' để kiểm chứng chỉ như bình thường.
|
||||
|
||||
Dùng làm tham số ``verify=`` của ``requests`` cho mọi lượt gọi HTTPS."""
|
||||
return (self.data.get("tls_ca_bundle") or "").strip()
|
||||
|
||||
@ca_bundle.setter
|
||||
def ca_bundle(self, value: str) -> None:
|
||||
self.data["tls_ca_bundle"] = (value or "").strip()
|
||||
|
||||
# ---- MS365 -----------------------------------------------------------
|
||||
|
||||
@property
|
||||
def ms365(self) -> Dict[str, Any]:
|
||||
return self.data.setdefault("ms365", copy.deepcopy(self._defaults["ms365"]))
|
||||
|
||||
def ms365_try_unlock(self, code: str) -> bool:
|
||||
"""Mở khoá nhóm MS365 trong Cài đặt cho phiên này.
|
||||
|
||||
Đây là khoá phía giao diện (chặn bấm nhầm vào một mục nhạy cảm), KHÔNG
|
||||
phải xác thực Microsoft. Không bao giờ được lưu ở trạng thái đã mở."""
|
||||
if (code or "") and code == self.ms365.get("unlock_code", ""):
|
||||
self.data["ms365"]["unlocked"] = True
|
||||
return True
|
||||
return False
|
||||
|
||||
def ms365_lock(self) -> None:
|
||||
self.data.setdefault("ms365", {})["unlocked"] = False
|
||||
|
||||
# ---- nhóm cấu hình đọc thẳng ------------------------------------------
|
||||
|
||||
@property
|
||||
def code(self) -> Dict[str, Any]:
|
||||
return self.data["code"]
|
||||
|
||||
@property
|
||||
def teams(self) -> Dict[str, Any]:
|
||||
return self.data["teams"]
|
||||
|
||||
@property
|
||||
def history(self) -> Dict[str, Any]:
|
||||
return self.data["history"]
|
||||
|
||||
@property
|
||||
def codebase_memory(self) -> Dict[str, Any]:
|
||||
return self.data["codebase_memory"]
|
||||
|
||||
@property
|
||||
def cowork(self) -> Dict[str, Any]:
|
||||
return self.data["cowork"]
|
||||
|
||||
@property
|
||||
def mcp_servers(self) -> list:
|
||||
return self.data.setdefault("mcp_servers", [])
|
||||
|
||||
@property
|
||||
def structure(self) -> Dict[str, Any]:
|
||||
return self.data.setdefault("structure", {"max_nodes": 400, "max_edges": 400})
|
||||
|
||||
@property
|
||||
def monitoring_visibility(self) -> Dict[str, bool]:
|
||||
return self.data.setdefault(
|
||||
"monitoring_visibility",
|
||||
copy.deepcopy(self._defaults["monitoring_visibility"]))
|
||||
|
||||
@property
|
||||
def ext_connectors(self) -> Dict[str, list]:
|
||||
"""Connector (MCP) gom theo nhóm CAD/CAE/MS365/Other."""
|
||||
d = self.data.setdefault(
|
||||
"ext_connectors", {"cad": [], "cae": [], "ms365": [], "other": []})
|
||||
for cat in ("cad", "cae", "ms365", "other"):
|
||||
d.setdefault(cat, [])
|
||||
return d
|
||||
|
||||
# ---- công tắc tổng cho connector -------------------------------------
|
||||
|
||||
@property
|
||||
def connect_external(self) -> bool:
|
||||
"""Tắt cái này là agent không nối tới connector ngoài nào cả. Mặc định
|
||||
BẬT để cấu hình đang chạy không đổi hành vi."""
|
||||
return bool(self.data.setdefault("tools", {}).get("connect_external", True))
|
||||
|
||||
def set_connect_external(self, enabled: bool) -> None:
|
||||
self.data.setdefault("tools", {})["connect_external"] = bool(enabled)
|
||||
self.save()
|
||||
|
||||
# ---- những thứ đã gieo sẵn -------------------------------------------
|
||||
|
||||
@property
|
||||
def seeded_library_skills(self) -> list:
|
||||
"""Slug của skill thư viện đã gieo — để cái người dùng xoá đi không bị
|
||||
lặng lẽ gieo lại."""
|
||||
return list(self.data.setdefault("seeded_library_skills", []))
|
||||
|
||||
@seeded_library_skills.setter
|
||||
def seeded_library_skills(self, slugs) -> None:
|
||||
self.data["seeded_library_skills"] = list(dict.fromkeys(slugs or []))
|
||||
|
||||
@property
|
||||
def seeded_builtin_flows(self) -> list:
|
||||
"""Id của flow Co4E dựng sẵn đã gieo (cùng quy tắc tôn trọng việc người
|
||||
dùng đã xoá như seeded_library_skills)."""
|
||||
return list(self.data.setdefault("seeded_builtin_flows", []))
|
||||
|
||||
@seeded_builtin_flows.setter
|
||||
def seeded_builtin_flows(self, ids) -> None:
|
||||
self.data["seeded_builtin_flows"] = list(dict.fromkeys(ids or []))
|
||||
|
||||
# ---- định tuyến theo từng bề mặt chat --------------------------------
|
||||
|
||||
def routing_mode_for(self, surface: str) -> str:
|
||||
"""Chế độ có hiệu lực cho một bề mặt chat.
|
||||
|
||||
Đặt riêng cho bề mặt thì thắng; để trống thì lấy ``switch_mode`` chung.
|
||||
Giá trị lạ rơi về "off" — định tuyến luôn là thứ phải bật, kể cả khi
|
||||
có người sửa tay file cấu hình."""
|
||||
routing = self.routing
|
||||
override = (routing.get("surface_modes", {}) or {}).get(surface, "")
|
||||
mode = override or routing.get("switch_mode", "off")
|
||||
return mode if mode in self.ROUTING_MODES else "off"
|
||||
|
||||
def set_routing_mode_for(self, surface: str, mode: str) -> None:
|
||||
mode = mode if mode in self.ROUTING_MODES else "off"
|
||||
self.routing.setdefault("surface_modes", {})[surface] = mode
|
||||
self.save()
|
||||
|
||||
# ---- tiện ích --------------------------------------------------------
|
||||
|
||||
def model_label(self) -> str:
|
||||
return str(self.provider_conf().get("model", "?"))
|
||||
|
||||
# ---- ghi -------------------------------------------------------------
|
||||
def save(self) -> None:
|
||||
"""Ghi nguyên tử. Không bao giờ để lộ trạng thái mở khoá ms365."""
|
||||
to_write = self.data
|
||||
if self.data.get("ms365", {}).get("unlocked"):
|
||||
to_write = copy.deepcopy(self.data)
|
||||
to_write["ms365"]["unlocked"] = False
|
||||
to_write.pop("_project_history_dir", None)
|
||||
to_write["schema_version"] = CURRENT_VERSION
|
||||
self._file.write(to_write)
|
||||
|
||||
|
||||
def _deep_merge(base: Dict[str, Any], override: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""Trộn sâu — giống hệt ``config.py::_deep_merge``.
|
||||
|
||||
Không import lại từ đó vì file này phải sống được sau khi ``config.py``
|
||||
biến mất; giữ bản sao 6 dòng còn hơn giữ một sợi dây phụ thuộc.
|
||||
"""
|
||||
out = copy.deepcopy(base)
|
||||
for key, value in (override or {}).items():
|
||||
if isinstance(value, dict) and isinstance(out.get(key), dict):
|
||||
out[key] = _deep_merge(out[key], value)
|
||||
else:
|
||||
out[key] = value
|
||||
return out
|
||||
@@ -0,0 +1,134 @@
|
||||
"""Đánh số phiên bản và chuyển đổi cấu hình — R02-T06.
|
||||
|
||||
Hôm nay ``config.json`` không có số phiên bản. Nghĩa là không có cách nào biết
|
||||
file trên đĩa thuộc thời nào, và mọi thay đổi hình dạng phải xử lý bằng cách
|
||||
đoán — ``config.py::_migrate_connectors()`` chính là một ví dụ: nó đoán "có
|
||||
khoá ``office`` nghĩa là file cũ".
|
||||
|
||||
Ở đây đặt luật rõ:
|
||||
|
||||
* File có ``schema_version``. Thiếu ⇒ coi là **1** (mọi file đang tồn tại).
|
||||
* Mỗi bước nâng cấp là một hàm ``v1 -> v2``, chạy tuần tự, không nhảy cóc.
|
||||
* **Sao lưu trước khi nâng cấp.** Người dùng lùi về bản app cũ thì bản cũ đọc
|
||||
file mới có thể hỏng — phải còn đường về.
|
||||
* Chỉ nâng, không hạ. File mới hơn app thì báo và dùng nguyên trạng, không cố
|
||||
đoán ngược.
|
||||
|
||||
Bước v1→v2 đầu tiên đi kèm R02-T05: gỡ ``api_key`` khỏi đĩa, đẩy vào
|
||||
``SecretStore``.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import copy
|
||||
import logging
|
||||
import shutil
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, Callable, Dict
|
||||
|
||||
from ..secrets.secret_store import SecretStore, provider_key
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
#: Phiên bản app hiện đang ghi ra.
|
||||
CURRENT_VERSION = 2
|
||||
|
||||
#: Thiếu ``schema_version`` ⇒ file có từ trước khi đánh số.
|
||||
ASSUMED_VERSION = 1
|
||||
|
||||
|
||||
def read_version(data: Dict[str, Any]) -> int:
|
||||
try:
|
||||
return int(data.get("schema_version", ASSUMED_VERSION))
|
||||
except (TypeError, ValueError):
|
||||
return ASSUMED_VERSION
|
||||
|
||||
|
||||
def _v1_to_v2(data: Dict[str, Any], secrets: SecretStore | None) -> Dict[str, Any]:
|
||||
"""Chuyển API key từ file sang kho bí mật — R02-T05.
|
||||
|
||||
Không có kho bí mật thì **không chuyển**: thà để khoá nằm nguyên trong file
|
||||
còn hơn xoá đi rồi người dùng mất khoá mà không hiểu vì sao. File giữ
|
||||
nguyên phiên bản 1, lần chạy sau trên máy có keyring sẽ chuyển.
|
||||
"""
|
||||
if secrets is None or not getattr(secrets, "available", True):
|
||||
log.info("bỏ qua v1→v2: máy này chưa có kho bí mật dùng được")
|
||||
return data
|
||||
|
||||
out = copy.deepcopy(data)
|
||||
moved = []
|
||||
for name, conf in (out.get("providers") or {}).items():
|
||||
if not isinstance(conf, dict):
|
||||
continue
|
||||
key = (conf.get("api_key") or "").strip()
|
||||
# "ollama" là giá trị bù nhìn — Ollama đòi có api_key nhưng bỏ qua nội
|
||||
# dung. Đẩy nó vào keyring chỉ tổ rác.
|
||||
if not key or key == "ollama":
|
||||
continue
|
||||
secrets.set(provider_key(name), key)
|
||||
conf["api_key"] = ""
|
||||
moved.append(name)
|
||||
|
||||
out["schema_version"] = 2
|
||||
if moved:
|
||||
log.info("đã chuyển API key sang kho bí mật: %s", ", ".join(moved))
|
||||
return out
|
||||
|
||||
|
||||
#: {phiên bản nguồn: hàm nâng lên phiên bản kế tiếp}
|
||||
STEPS: Dict[int, Callable[[Dict[str, Any], SecretStore | None], Dict[str, Any]]] = {
|
||||
1: _v1_to_v2,
|
||||
}
|
||||
|
||||
|
||||
def backup(path: Path) -> Path | None:
|
||||
"""Chép file trước khi nâng cấp. Trả về đường dẫn bản sao."""
|
||||
if not path.exists():
|
||||
return None
|
||||
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
target = path.with_suffix(path.suffix + f".v{stamp}.bak")
|
||||
try:
|
||||
shutil.copy2(path, target)
|
||||
return target
|
||||
except OSError as exc:
|
||||
log.warning("không sao lưu được %s: %s", path, exc)
|
||||
return None
|
||||
|
||||
|
||||
def migrate(data: Dict[str, Any], *, secrets: SecretStore | None = None,
|
||||
path: Path | None = None) -> tuple[Dict[str, Any], bool]:
|
||||
"""Nâng ``data`` lên :data:`CURRENT_VERSION`.
|
||||
|
||||
Trả về ``(dữ_liệu, có_đổi_không)``. ``có_đổi_không`` là False thì chỗ gọi
|
||||
khỏi phải ghi lại đĩa.
|
||||
"""
|
||||
version = read_version(data)
|
||||
|
||||
if version > CURRENT_VERSION:
|
||||
# App cũ gặp file mới. Đoán ngược là cách nhanh nhất để mất dữ liệu.
|
||||
log.warning("config phiên bản %s mới hơn app (%s) — dùng nguyên trạng",
|
||||
version, CURRENT_VERSION)
|
||||
return data, False
|
||||
|
||||
if version == CURRENT_VERSION:
|
||||
return data, False
|
||||
|
||||
if path is not None:
|
||||
backup(path)
|
||||
|
||||
changed = False
|
||||
while version < CURRENT_VERSION:
|
||||
step = STEPS.get(version)
|
||||
if step is None:
|
||||
log.warning("thiếu bước nâng cấp từ phiên bản %s — dừng", version)
|
||||
break
|
||||
data = step(data, secrets)
|
||||
new_version = read_version(data)
|
||||
if new_version <= version:
|
||||
# Bước không nâng được phiên bản (ví dụ v1→v2 bỏ qua vì chưa có
|
||||
# keyring). Dừng, đừng lặp vô hạn.
|
||||
break
|
||||
version = new_version
|
||||
changed = True
|
||||
|
||||
return data, changed
|
||||
@@ -0,0 +1,178 @@
|
||||
"""Khung nhìn có kiểu cho từng nhóm cấu hình — R02-T03.
|
||||
|
||||
Vấn đề đang có: khắp nơi viết ``ctx.config.routing.get("switch_mode", "off")``.
|
||||
Gõ sai một chữ thì lặng lẽ nhận giá trị mặc định, không ai biết cho tới khi
|
||||
tính năng "không hiểu sao không chạy". Đếm được **156 lời gọi ``ctx.config.*``
|
||||
trong 29 file** kiểu đó.
|
||||
|
||||
Ở đây mỗi nhóm cấu hình có một lớp: gõ sai tên thuộc tính là lỗi ngay, và kiểu
|
||||
dữ liệu ghi rõ ràng nên đọc code là biết ``confirm_timeout_sec`` là số giây
|
||||
chứ không phải mili giây.
|
||||
|
||||
Cố ý KHÔNG dùng dataclass đông cứng: đây là *khung nhìn* lên dict cấu hình
|
||||
sống, sửa qua đây là sửa vào dict rồi ``save()`` là xuống đĩa. Sao chép thành
|
||||
dataclass thì lại sinh chuyện đồng bộ hai chiều.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict
|
||||
|
||||
|
||||
class _View:
|
||||
"""Khung nhìn lên một nhánh của dict cấu hình."""
|
||||
|
||||
def __init__(self, data: Dict[str, Any]):
|
||||
self._d = data
|
||||
|
||||
def _get(self, key: str, default: Any) -> Any:
|
||||
value = self._d.get(key, default)
|
||||
return default if value is None else value
|
||||
|
||||
def raw(self) -> Dict[str, Any]:
|
||||
"""Dict gốc — dùng khi cần đọc khoá chưa được đưa vào khung nhìn.
|
||||
|
||||
Có mặt để không ai bị kẹt: thiếu thuộc tính thì dùng tạm ``raw()`` rồi
|
||||
mở issue bổ sung, chứ đừng vòng lại ``ctx.config.data``.
|
||||
"""
|
||||
return self._d
|
||||
|
||||
|
||||
class ProviderSettings(_View):
|
||||
"""Một provider: đi đâu, model nào, khoá nào.
|
||||
|
||||
``api_key`` ở đây là thứ ``JsonConfigRepository.provider_conf()`` đã ghép
|
||||
sẵn từ kho bí mật — xem đường A trong ``GammaTeam_decisions.md``.
|
||||
"""
|
||||
|
||||
@property
|
||||
def base_url(self) -> str:
|
||||
return str(self._get("base_url", ""))
|
||||
|
||||
@property
|
||||
def model(self) -> str:
|
||||
return str(self._get("model", ""))
|
||||
|
||||
@property
|
||||
def api_key(self) -> str:
|
||||
return str(self._get("api_key", ""))
|
||||
|
||||
@property
|
||||
def configured(self) -> bool:
|
||||
"""Đủ thông tin để gọi được chưa.
|
||||
|
||||
Ollama chạy cục bộ nên không cần khoá — đó là lý do điều kiện là
|
||||
"có base_url và model", không phải "có api_key".
|
||||
"""
|
||||
return bool(self.base_url and self.model)
|
||||
|
||||
|
||||
class RoutingSettings(_View):
|
||||
"""Định tuyến model tự động (``core/routing/``)."""
|
||||
|
||||
@property
|
||||
def switch_mode(self) -> str:
|
||||
"""``"off"`` | ``"auto"`` | ``"manual"``."""
|
||||
return str(self._get("switch_mode", "off"))
|
||||
|
||||
@switch_mode.setter
|
||||
def switch_mode(self, value: str) -> None:
|
||||
self._d["switch_mode"] = value
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
return self.switch_mode != "off"
|
||||
|
||||
@property
|
||||
def policy(self) -> str:
|
||||
"""``"balanced"`` | ``"cheap"`` | ``"quality"``…"""
|
||||
return str(self._get("policy", "balanced"))
|
||||
|
||||
@property
|
||||
def min_score_gain(self) -> float:
|
||||
"""Phải hơn model hiện tại bao nhiêu điểm mới đáng đổi."""
|
||||
return float(self._get("min_score_gain", 0.05))
|
||||
|
||||
@property
|
||||
def confirm_timeout_sec(self) -> int:
|
||||
"""GIÂY, không phải mili giây — đọc tên là biết, khỏi phải mò."""
|
||||
return int(self._get("confirm_timeout_sec", 60))
|
||||
|
||||
@property
|
||||
def reassess_interval_hours(self) -> int:
|
||||
return int(self._get("reassess_interval_hours", 24))
|
||||
|
||||
@property
|
||||
def per_provider_concurrency(self) -> int:
|
||||
return int(self._get("per_provider_concurrency", 2))
|
||||
|
||||
@property
|
||||
def judge_provider(self) -> str:
|
||||
return str(self._get("judge_provider", ""))
|
||||
|
||||
@property
|
||||
def judge_model(self) -> str:
|
||||
return str(self._get("judge_model", ""))
|
||||
|
||||
|
||||
class SecuritySettings(_View):
|
||||
"""Chính sách an toàn cho agent (``core/agent_security.py``)."""
|
||||
|
||||
@property
|
||||
def enabled(self) -> bool:
|
||||
return bool(self._get("enabled", True))
|
||||
|
||||
@property
|
||||
def validate_prompt(self) -> bool:
|
||||
return bool(self._get("validate_prompt", True))
|
||||
|
||||
@property
|
||||
def validate_attachments(self) -> bool:
|
||||
return bool(self._get("validate_attachments", True))
|
||||
|
||||
@property
|
||||
def validate_commands(self) -> bool:
|
||||
return bool(self._get("validate_commands", True))
|
||||
|
||||
@property
|
||||
def command_ai_check(self) -> bool:
|
||||
return bool(self._get("command_ai_check", False))
|
||||
|
||||
@property
|
||||
def cowork_confirm_commands(self) -> bool:
|
||||
"""Có hỏi trước khi chạy lệnh không.
|
||||
|
||||
Ứng với ``PolicyOutcome.ASK`` trong
|
||||
``domain/security/tool_policy.py``.
|
||||
"""
|
||||
return bool(self._get("cowork_confirm_commands", True))
|
||||
|
||||
@property
|
||||
def rules_onedrive_url(self) -> str:
|
||||
return str(self._get("rules_onedrive_url", ""))
|
||||
|
||||
@property
|
||||
def admin_email(self) -> str:
|
||||
return str(self._get("admin_email", ""))
|
||||
|
||||
|
||||
class Settings:
|
||||
"""Cửa vào duy nhất cho các nhóm cấu hình có kiểu.
|
||||
|
||||
>>> s = Settings(repo)
|
||||
>>> if s.routing.enabled and s.provider().configured:
|
||||
... ...
|
||||
"""
|
||||
|
||||
def __init__(self, repo):
|
||||
self._repo = repo
|
||||
|
||||
def provider(self, name: str | None = None) -> ProviderSettings:
|
||||
return ProviderSettings(self._repo.provider_conf(name))
|
||||
|
||||
@property
|
||||
def routing(self) -> RoutingSettings:
|
||||
return RoutingSettings(self._repo.routing)
|
||||
|
||||
@property
|
||||
def security(self) -> SecuritySettings:
|
||||
return SecuritySettings(self._repo.agent_security)
|
||||
@@ -0,0 +1 @@
|
||||
"""Infrastructure filesystem package: Tool handlers (file, command, fetch tools) and execution workspace."""
|
||||
@@ -0,0 +1 @@
|
||||
"""Infrastructure MCP package: McpToolSourceManager and child process lifecycle."""
|
||||
@@ -0,0 +1 @@
|
||||
"""Infrastructure persistence package."""
|
||||
@@ -0,0 +1 @@
|
||||
"""Infrastructure JSON persistence package: AtomicJsonFile and repositories."""
|
||||
@@ -0,0 +1,131 @@
|
||||
"""Ghi JSON kiểu không-hỏng-file — R02-T01.
|
||||
|
||||
Vấn đề đang có: ``config.py::save()`` gọi thẳng ``path.write_text(...)``. Hàm
|
||||
đó mở file, cắt cụt về 0 byte, rồi mới ghi nội dung mới. Mất điện, tắt máy, hay
|
||||
process bị kill đúng khoảng giữa thì file cấu hình còn lại **rỗng hoặc ghi dở**
|
||||
— và người dùng mất toàn bộ cấu hình.
|
||||
|
||||
Cách làm ở đây theo đúng thứ tự bắt buộc:
|
||||
|
||||
1. Ghi vào file tạm cùng thư mục (phải cùng ổ đĩa thì bước 3 mới nguyên tử)
|
||||
2. ``flush()`` + ``os.fsync()`` — ép dữ liệu xuống đĩa thật, không nằm trong
|
||||
bộ đệm của hệ điều hành
|
||||
3. ``os.replace()`` — nguyên tử trên cả Windows lẫn POSIX
|
||||
|
||||
Bất kỳ lúc nào chết giữa chừng, file đích vẫn là **bản cũ nguyên vẹn**. Không
|
||||
bao giờ có trạng thái ghi dở.
|
||||
|
||||
Phần đọc có chính sách phục hồi: file hỏng thì giữ lại thành ``.bad`` để còn
|
||||
cứu tay, rồi trả về giá trị mặc định — hỏng cấu hình không được chặn khởi động,
|
||||
đúng như ``config.py`` hiện tại đang làm.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
|
||||
class AtomicJsonFile:
|
||||
"""Một file JSON, đọc ghi an toàn.
|
||||
|
||||
>>> f = AtomicJsonFile(Path("cau_hinh.json"))
|
||||
>>> f.write({"theme": "dark"})
|
||||
>>> f.read(default={})
|
||||
{'theme': 'dark'}
|
||||
"""
|
||||
|
||||
def __init__(self, path: Path, *, indent: int = 2):
|
||||
self.path = Path(path)
|
||||
self.indent = indent
|
||||
|
||||
# ---- đọc ------------------------------------------------------------
|
||||
def read(self, default: Any = None) -> Any:
|
||||
"""Nội dung file, hoặc ``default`` nếu chưa có / hỏng.
|
||||
|
||||
Không ném lỗi. File hỏng được đổi tên thành ``<tên>.bad-<thời điểm>``
|
||||
rồi mới trả mặc định — hỏng thì cứu được, chứ đừng ghi đè im lặng.
|
||||
"""
|
||||
if not self.path.exists():
|
||||
return default
|
||||
try:
|
||||
return json.loads(self.path.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||
self._quarantine()
|
||||
return default
|
||||
except OSError:
|
||||
# Không đọc được (khoá file, mất quyền) — KHÔNG cách ly, vì file
|
||||
# có thể vẫn tốt nguyên.
|
||||
return default
|
||||
|
||||
def _quarantine(self) -> Path | None:
|
||||
stamp = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||||
target = self.path.with_suffix(self.path.suffix + f".bad-{stamp}")
|
||||
try:
|
||||
os.replace(self.path, target)
|
||||
return target
|
||||
except OSError:
|
||||
return None
|
||||
|
||||
# ---- ghi ------------------------------------------------------------
|
||||
#: Số lần thử lại ``os.replace`` và khoảng nghỉ giữa các lần (giây).
|
||||
_REPLACE_TRIES = 6
|
||||
_REPLACE_BACKOFF = 0.02
|
||||
|
||||
@classmethod
|
||||
def _replace_ben_bi(cls, src: Path, dst: Path) -> None:
|
||||
"""``os.replace`` có thử lại — bắt buộc trên Windows.
|
||||
|
||||
MoveFileEx trả ERROR_ACCESS_DENIED khi có tiến trình khác đang giữ
|
||||
handle lên nguồn hoặc đích. Trên Windows thật thì gần như luôn là
|
||||
Defender hoặc Search Indexer quét file vừa tạo, giữ handle vài chục
|
||||
mili-giây rồi nhả. Không phải lỗi quyền thật, thử lại là hết.
|
||||
|
||||
Đo trên máy dev 25/08: hỏng 1 trong 7 lượt chạy 20 lần ghi, tức
|
||||
khoảng 1 trên 140 lần lưu. Không có vòng này thì người dùng thỉnh
|
||||
thoảng bấm Lưu là văng lỗi mà không tài nào tái hiện.
|
||||
|
||||
POSIX không có kiểu hỏng này nên vòng lặp chạy đúng một lượt.
|
||||
"""
|
||||
for lan in range(cls._REPLACE_TRIES):
|
||||
try:
|
||||
os.replace(src, dst)
|
||||
return
|
||||
except PermissionError:
|
||||
if lan == cls._REPLACE_TRIES - 1:
|
||||
raise
|
||||
time.sleep(cls._REPLACE_BACKOFF * (2 ** lan))
|
||||
|
||||
def write(self, data: Any) -> None:
|
||||
"""Ghi ``data``. Hoặc thành công trọn vẹn, hoặc file cũ còn nguyên."""
|
||||
self.path.parent.mkdir(parents=True, exist_ok=True)
|
||||
text = json.dumps(data, indent=self.indent, ensure_ascii=False)
|
||||
|
||||
# File tạm phải nằm CÙNG thư mục: os.replace chỉ nguyên tử trong cùng
|
||||
# một hệ thống tệp. Để ở %TEMP% là có thể rơi sang ổ khác và biến
|
||||
# thành copy + delete — mất luôn tính nguyên tử.
|
||||
fd, tmp_name = tempfile.mkstemp(
|
||||
dir=str(self.path.parent), prefix=f".{self.path.name}.", suffix=".tmp")
|
||||
tmp = Path(tmp_name)
|
||||
try:
|
||||
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
||||
f.write(text)
|
||||
f.flush()
|
||||
os.fsync(f.fileno()) # xuống đĩa thật, không chỉ vào bộ đệm
|
||||
self._replace_ben_bi(tmp, self.path) # nguyên tử, có thử lại
|
||||
except BaseException:
|
||||
# Kể cả KeyboardInterrupt/SystemExit cũng phải dọn file tạm, đừng
|
||||
# để rác .tmp nằm lại cạnh file cấu hình.
|
||||
tmp.unlink(missing_ok=True)
|
||||
raise
|
||||
|
||||
# ---- tiện ích -------------------------------------------------------
|
||||
def exists(self) -> bool:
|
||||
return self.path.exists()
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"AtomicJsonFile({self.path})"
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user