mirror of
https://github.com/eigent-ai/eigent.git
synced 2026-05-26 07:25:58 +00:00
feat: add @mention routing for direct single-agent conversation
- Add @mention dropdown UI in input box (keyboard navigable)
- Parse {{@agentId}} tags in message content for mention badge rendering
- Backend: route @agent messages directly to persistent agents, skip workforce
- Backend: reuse persistent agents across turns (preserve toolkit state)
- Frontend: persist mention target across turns, render in input and chat bubbles
- Fix keyboard ArrowUp/Down selection in mention dropdown
This commit is contained in:
parent
fb740bbe3f
commit
26a00eb2f9
13 changed files with 843 additions and 52 deletions
|
|
@ -189,6 +189,25 @@ def browser_agent(options: Chat):
|
|||
else:
|
||||
selected_port = env("browser_port", "9222")
|
||||
|
||||
enabled_browser_tools = [
|
||||
"browser_click",
|
||||
"browser_type",
|
||||
"browser_back",
|
||||
"browser_forward",
|
||||
"browser_select",
|
||||
"browser_console_exec",
|
||||
"browser_console_view",
|
||||
"browser_switch_tab",
|
||||
"browser_enter",
|
||||
"browser_visit_page",
|
||||
"browser_scroll",
|
||||
"browser_sheet_read",
|
||||
"browser_sheet_input",
|
||||
"browser_get_page_snapshot",
|
||||
]
|
||||
if selected_is_external:
|
||||
enabled_browser_tools.append("browser_open")
|
||||
|
||||
web_toolkit_custom = HybridBrowserToolkit(
|
||||
options.project_id,
|
||||
cdp_keep_current_page=True,
|
||||
|
|
@ -197,23 +216,7 @@ def browser_agent(options: Chat):
|
|||
stealth=True,
|
||||
session_id=toolkit_session_id,
|
||||
cdp_url=f"http://localhost:{selected_port}",
|
||||
enabled_tools=[
|
||||
"browser_click",
|
||||
"browser_type",
|
||||
"browser_back",
|
||||
"browser_forward",
|
||||
"browser_select",
|
||||
"browser_console_exec",
|
||||
"browser_console_view",
|
||||
"browser_switch_tab",
|
||||
"browser_enter",
|
||||
"browser_visit_page",
|
||||
"browser_scroll",
|
||||
"browser_sheet_read",
|
||||
"browser_sheet_input",
|
||||
"browser_get_page_snapshot",
|
||||
"browser_open",
|
||||
],
|
||||
enabled_tools=enabled_browser_tools,
|
||||
)
|
||||
|
||||
# Save reference before registering for toolkits_to_register_agent
|
||||
|
|
|
|||
|
|
@ -342,6 +342,7 @@ def improve(id: str, data: SupplementChat):
|
|||
data=ImprovePayload(
|
||||
question=data.question,
|
||||
attaches=data.attaches or [],
|
||||
target=data.target,
|
||||
),
|
||||
new_task_id=data.task_id,
|
||||
)
|
||||
|
|
@ -349,7 +350,7 @@ def improve(id: str, data: SupplementChat):
|
|||
)
|
||||
chat_logger.info(
|
||||
"Improvement request queued with preserved context",
|
||||
extra={"project_id": id},
|
||||
extra={"project_id": id, "target": data.target},
|
||||
)
|
||||
return Response(status_code=201)
|
||||
|
||||
|
|
|
|||
|
|
@ -78,6 +78,9 @@ class Chat(BaseModel):
|
|||
search_config: dict[str, str] | None = None
|
||||
# User identifier for user-specific skill configurations
|
||||
user_id: str | None = None
|
||||
# Target agent for @mention routing: "browser", "dev", "doc",
|
||||
# "media", "workforce", or None (default behavior)
|
||||
target: str | None = None
|
||||
|
||||
@field_validator("model_type")
|
||||
@classmethod
|
||||
|
|
@ -141,6 +144,7 @@ class SupplementChat(BaseModel):
|
|||
question: str
|
||||
task_id: str | None = None
|
||||
attaches: list[str] = []
|
||||
target: str | None = None
|
||||
|
||||
|
||||
class HumanReply(BaseModel):
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ from app.agent.factory import (
|
|||
mcp_agent,
|
||||
multi_modal_agent,
|
||||
question_confirm_agent,
|
||||
social_media_agent,
|
||||
task_summary_agent,
|
||||
)
|
||||
from app.agent.listen_chat_agent import ListenChatAgent
|
||||
|
|
@ -48,6 +49,7 @@ from app.service.task import (
|
|||
Action,
|
||||
ActionDecomposeProgressData,
|
||||
ActionDecomposeTextData,
|
||||
ActionEndData,
|
||||
ActionImproveData,
|
||||
ActionInstallMcpData,
|
||||
ActionNewAgent,
|
||||
|
|
@ -284,6 +286,98 @@ def build_context_for_workforce(
|
|||
)
|
||||
|
||||
|
||||
# ================================================================
|
||||
# @mention direct agent routing
|
||||
# ================================================================
|
||||
|
||||
# Maps @mention target names to (factory_fn, is_async) pairs
|
||||
_AGENT_TARGET_MAP: dict[str, tuple] = {
|
||||
"browser": (browser_agent, False),
|
||||
"dev": (developer_agent, True),
|
||||
"doc": (document_agent, True),
|
||||
"media": (multi_modal_agent, False),
|
||||
"social": (social_media_agent, True),
|
||||
}
|
||||
|
||||
|
||||
async def _create_persistent_agent(
|
||||
target: str, options: Chat
|
||||
) -> ListenChatAgent:
|
||||
"""Create a persistent agent by target name using existing factories."""
|
||||
if target not in _AGENT_TARGET_MAP:
|
||||
raise ValueError(
|
||||
f"Unknown agent target: {target}. "
|
||||
f"Valid targets: {list(_AGENT_TARGET_MAP.keys())}"
|
||||
)
|
||||
factory_fn, is_async = _AGENT_TARGET_MAP[target]
|
||||
if is_async:
|
||||
agent = await factory_fn(options)
|
||||
else:
|
||||
agent = await asyncio.to_thread(factory_fn, options)
|
||||
logger.info(
|
||||
f"[DIRECT-AGENT] Created persistent agent: {target}",
|
||||
extra={
|
||||
"project_id": options.project_id,
|
||||
"agent_name": getattr(agent, "agent_name", target),
|
||||
"agent_id": getattr(agent, "agent_id", ""),
|
||||
},
|
||||
)
|
||||
return agent
|
||||
|
||||
|
||||
async def _run_direct_agent(
|
||||
agent,
|
||||
prompt: str,
|
||||
question: str,
|
||||
task_lock: TaskLock,
|
||||
):
|
||||
"""Background task that runs a direct agent step.
|
||||
|
||||
agent.astep() internally sends activate_agent and deactivate_agent
|
||||
events via the queue, so step_solve's main loop can process them
|
||||
in real-time alongside toolkit events.
|
||||
|
||||
After completion, puts ActionEndData into the queue so step_solve
|
||||
yields the 'end' SSE event.
|
||||
"""
|
||||
from camel.agents.chat_agent import (
|
||||
AsyncStreamingChatAgentResponse,
|
||||
)
|
||||
|
||||
response_content = ""
|
||||
try:
|
||||
response = await agent.astep(prompt)
|
||||
if isinstance(response, AsyncStreamingChatAgentResponse):
|
||||
# Must consume the stream to trigger deactivation
|
||||
# in _astream_chunks's finally block
|
||||
async for chunk in response:
|
||||
if chunk.msg and chunk.msg.content:
|
||||
response_content += chunk.msg.content
|
||||
else:
|
||||
response_content = response.msg.content if response.msg else ""
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"[DIRECT-AGENT] Error executing agent: {e}",
|
||||
exc_info=True,
|
||||
)
|
||||
response_content = f"Error executing agent: {e}"
|
||||
|
||||
# Save conversation history
|
||||
task_lock.add_conversation("user", question)
|
||||
task_lock.add_conversation("assistant", response_content)
|
||||
|
||||
# Yield control so the agent's deactivate_agent event
|
||||
# (scheduled via _schedule_async_task in _astream_chunks's
|
||||
# finally block) fires before the end event.
|
||||
# Without this, end arrives first and frontend ignores
|
||||
# deactivate_agent because task is already FINISHED.
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
# Signal completion — step_solve's Action.end handler
|
||||
# will yield sse_json("end", ...) and set status to done
|
||||
await task_lock.put_queue(ActionEndData(data=response_content[:300]))
|
||||
|
||||
|
||||
@sync_step
|
||||
async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
|
||||
"""Main task execution loop. Called when POST /chat endpoint
|
||||
|
|
@ -469,6 +563,135 @@ async def step_solve(options: Chat, request: Request, task_lock: TaskLock):
|
|||
f"'{question[:100]}...'"
|
||||
)
|
||||
|
||||
# --- @mention direct agent routing ---
|
||||
target: str | None = None
|
||||
if (
|
||||
hasattr(options, "target")
|
||||
and options.target
|
||||
and loop_iteration == 1
|
||||
):
|
||||
target = options.target
|
||||
elif isinstance(item, ActionImproveData) and item.data.target:
|
||||
target = item.data.target
|
||||
|
||||
if (
|
||||
target
|
||||
and target != "workforce"
|
||||
and target in _AGENT_TARGET_MAP
|
||||
):
|
||||
# Direct agent mode: keep the SAME task_id
|
||||
# across turns (continuous chat in one chatStore).
|
||||
# Do NOT update options.task_id here — the
|
||||
# frontend reuses the existing chatStore.
|
||||
|
||||
logger.info(
|
||||
f"[DIRECT-AGENT] Routing to @{target}",
|
||||
extra={
|
||||
"project_id": options.project_id,
|
||||
"target": target,
|
||||
"task_id": options.task_id,
|
||||
},
|
||||
)
|
||||
|
||||
# Send confirmed event so frontend transitions
|
||||
# from pending/splitting state.
|
||||
# direct=True tells frontend to skip task
|
||||
# splitting and go straight to RUNNING.
|
||||
yield sse_json(
|
||||
"confirmed",
|
||||
{"question": question, "direct": True},
|
||||
)
|
||||
|
||||
# Ensure event loop is set for agent internals
|
||||
set_main_event_loop(asyncio.get_running_loop())
|
||||
|
||||
# Get or create persistent agent
|
||||
agent = task_lock.persistent_agents.get(target)
|
||||
is_new_agent = agent is None
|
||||
|
||||
logger.info(
|
||||
f"[DIRECT-AGENT] persistent_agents"
|
||||
f" keys: {list(task_lock.persistent_agents.keys())},"
|
||||
f" is_new={is_new_agent},"
|
||||
f" target={target}",
|
||||
)
|
||||
|
||||
if is_new_agent:
|
||||
# Factory internally sends create_agent
|
||||
# via ActionCreateAgentData in the queue
|
||||
agent = await _create_persistent_agent(target, options)
|
||||
task_lock.persistent_agents[target] = agent
|
||||
logger.info(
|
||||
f"[DIRECT-AGENT] Created NEW "
|
||||
f"agent: {agent.agent_name} "
|
||||
f"(id={agent.agent_id})",
|
||||
)
|
||||
else:
|
||||
logger.info(
|
||||
f"[DIRECT-AGENT] REUSING "
|
||||
f"agent: {agent.agent_name} "
|
||||
f"(id={agent.agent_id})",
|
||||
)
|
||||
# Reused agent: factory won't send
|
||||
# create_agent, so we must send it
|
||||
# explicitly for the new chatStore
|
||||
tool_names = []
|
||||
for t in getattr(agent, "tools", []):
|
||||
fn_name = getattr(t, "get_function_name", None)
|
||||
if fn_name:
|
||||
tool_names.append(fn_name())
|
||||
yield sse_json(
|
||||
"create_agent",
|
||||
{
|
||||
"agent_name": agent.agent_name,
|
||||
"agent_id": agent.agent_id,
|
||||
"tools": tool_names,
|
||||
},
|
||||
)
|
||||
|
||||
agent.process_task_id = options.task_id
|
||||
|
||||
# Build prompt: reused agents already have
|
||||
# conversation history in their CAMEL memory,
|
||||
# so only prepend context for brand-new agents.
|
||||
if is_new_agent:
|
||||
conv_ctx = build_conversation_context(
|
||||
task_lock,
|
||||
header="=== Previous Conversation ===",
|
||||
)
|
||||
prompt = f"{conv_ctx}\nUser: {question}"
|
||||
else:
|
||||
prompt = question
|
||||
if attaches_to_use:
|
||||
prompt += f"\n\nAttached files: {attaches_to_use}"
|
||||
|
||||
# Launch agent in background task so step_solve's
|
||||
# while loop can process queue events (activate,
|
||||
# toolkit, deactivate) in real-time.
|
||||
# agent.astep() internally sends activate_agent
|
||||
# and deactivate_agent via the queue.
|
||||
task = asyncio.create_task(
|
||||
_run_direct_agent(
|
||||
agent,
|
||||
prompt,
|
||||
question,
|
||||
task_lock,
|
||||
)
|
||||
)
|
||||
task_lock.add_background_task(task)
|
||||
continue
|
||||
|
||||
if target == "workforce":
|
||||
logger.info(
|
||||
"[DIRECT-AGENT] @workforce: "
|
||||
"cleaning up persistent agents",
|
||||
extra={
|
||||
"project_id": options.project_id,
|
||||
},
|
||||
)
|
||||
await task_lock.cleanup_persistent_agents()
|
||||
# --- end @mention routing ---
|
||||
|
||||
is_exceeded, total_length = check_conversation_history_length(
|
||||
task_lock
|
||||
)
|
||||
|
|
|
|||
|
|
@ -76,6 +76,7 @@ class ImprovePayload(BaseModel):
|
|||
|
||||
question: str
|
||||
attaches: list[str] = []
|
||||
target: str | None = None
|
||||
|
||||
|
||||
class ActionImproveData(BaseModel):
|
||||
|
|
@ -225,6 +226,7 @@ class ActionStopData(BaseModel):
|
|||
|
||||
class ActionEndData(BaseModel):
|
||||
action: Literal[Action.end] = Action.end
|
||||
data: str | None = None
|
||||
|
||||
|
||||
class ActionTimeoutData(BaseModel):
|
||||
|
|
@ -351,6 +353,8 @@ class TaskLock:
|
|||
"""Track if summary has been generated for this project"""
|
||||
current_task_id: str | None
|
||||
"""Current task ID to be used in SSE responses"""
|
||||
persistent_agents: dict[str, Any]
|
||||
"""Persistent agents for @mention direct chat (key: agent type name)"""
|
||||
|
||||
def __init__(
|
||||
self, id: str, queue: asyncio.Queue, human_input: dict
|
||||
|
|
@ -369,6 +373,7 @@ class TaskLock:
|
|||
self.last_task_summary = ""
|
||||
self.question_agent = None
|
||||
self.current_task_id = None
|
||||
self.persistent_agents = {}
|
||||
|
||||
logger.info(
|
||||
"Task lock initialized",
|
||||
|
|
@ -426,6 +431,35 @@ class TaskLock:
|
|||
self.background_tasks.add(task)
|
||||
task.add_done_callback(lambda t: self.background_tasks.discard(t))
|
||||
|
||||
async def cleanup_persistent_agents(self):
|
||||
r"""Release all persistent agents and their resources (e.g. CDP)."""
|
||||
if not self.persistent_agents:
|
||||
return
|
||||
logger.info(
|
||||
"Cleaning up persistent agents",
|
||||
extra={
|
||||
"task_id": self.id,
|
||||
"agents": list(self.persistent_agents.keys()),
|
||||
},
|
||||
)
|
||||
for name, agent in self.persistent_agents.items():
|
||||
try:
|
||||
if (
|
||||
hasattr(agent, "_cdp_release_callback")
|
||||
and agent._cdp_release_callback
|
||||
):
|
||||
agent._cdp_release_callback(agent)
|
||||
logger.info(
|
||||
f"Released CDP for persistent agent {name}",
|
||||
extra={"task_id": self.id},
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(
|
||||
f"Failed to release CDP for persistent agent {name}: {e}",
|
||||
extra={"task_id": self.id},
|
||||
)
|
||||
self.persistent_agents.clear()
|
||||
|
||||
async def cleanup(self):
|
||||
r"""Cancel all background tasks and clean up resources"""
|
||||
logger.info(
|
||||
|
|
@ -435,6 +469,10 @@ class TaskLock:
|
|||
"background_tasks_count": len(self.background_tasks),
|
||||
},
|
||||
)
|
||||
|
||||
# Clean up persistent agents first
|
||||
await self.cleanup_persistent_agents()
|
||||
|
||||
for task in list(self.background_tasks):
|
||||
if not task.done():
|
||||
task.cancel()
|
||||
|
|
|
|||
|
|
@ -352,6 +352,9 @@ app.commandLine.appendSwitch('max_old_space_size', '4096');
|
|||
app.commandLine.appendSwitch('enable-features', 'MemoryPressureReduction');
|
||||
app.commandLine.appendSwitch('renderer-process-limit', '8');
|
||||
|
||||
// Disable Fontations (Rust-based font engine) to prevent crashes on macOS
|
||||
app.commandLine.appendSwitch('disable-features', 'Fontations');
|
||||
|
||||
// ==================== Proxy configuration ====================
|
||||
// Read proxy from global .env file on startup
|
||||
proxyUrl = readGlobalEnvKey('HTTP_PROXY');
|
||||
|
|
|
|||
117
server/uv.lock
generated
117
server/uv.lock
generated
|
|
@ -1,5 +1,5 @@
|
|||
version = 1
|
||||
revision = 3
|
||||
revision = 2
|
||||
requires-python = "==3.12.*"
|
||||
resolution-markers = [
|
||||
"sys_platform == 'win32'",
|
||||
|
|
@ -223,6 +223,30 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/f3/2e/500ff29726ef207fdf6b625e62caf3839662c5d845897efc93bdf019192a/convert_case-1.2.3-py3-none-any.whl", hash = "sha256:ec8884050ca548e990666f82cba7ae2edfaa3c85dbead3042c2fd663b292373a", size = 9373, upload-time = "2023-05-23T19:27:06.039Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.13.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/24/56/95b7e30fa389756cb56630faa728da46a27b8c6eb46f9d557c68fff12b65/coverage-7.13.4.tar.gz", hash = "sha256:e5c8f6ed1e61a8b2dcdf31eb0b9bbf0130750ca79c1c49eb898e2ad86f5ccc91", size = 827239, upload-time = "2026-02-09T12:59:03.86Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/81/4ce2fdd909c5a0ed1f6dedb88aa57ab79b6d1fbd9b588c1ac7ef45659566/coverage-7.13.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:02231499b08dabbe2b96612993e5fc34217cdae907a51b906ac7fca8027a4459", size = 219449, upload-time = "2026-02-09T12:56:54.889Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/96/5238b1efc5922ddbdc9b0db9243152c09777804fb7c02ad1741eb18a11c0/coverage-7.13.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40aa8808140e55dc022b15d8aa7f651b6b3d68b365ea0398f1441e0b04d859c3", size = 219810, upload-time = "2026-02-09T12:56:56.33Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/72/2f372b726d433c9c35e56377cf1d513b4c16fe51841060d826b95caacec1/coverage-7.13.4-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5b856a8ccf749480024ff3bd7310adaef57bf31fd17e1bfc404b7940b6986634", size = 251308, upload-time = "2026-02-09T12:56:57.858Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5d/a0/2ea570925524ef4e00bb6c82649f5682a77fac5ab910a65c9284de422600/coverage-7.13.4-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c048ea43875fbf8b45d476ad79f179809c590ec7b79e2035c662e7afa3192e3", size = 254052, upload-time = "2026-02-09T12:56:59.754Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/ac/45dc2e19a1939098d783c846e130b8f862fbb50d09e0af663988f2f21973/coverage-7.13.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7b38448866e83176e28086674fe7368ab8590e4610fb662b44e345b86d63ffa", size = 255165, upload-time = "2026-02-09T12:57:01.287Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2d/4d/26d236ff35abc3b5e63540d3386e4c3b192168c1d96da5cb2f43c640970f/coverage-7.13.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:de6defc1c9badbf8b9e67ae90fd00519186d6ab64e5cc5f3d21359c2a9b2c1d3", size = 257432, upload-time = "2026-02-09T12:57:02.637Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/55/14a966c757d1348b2e19caf699415a2a4c4f7feaa4bbc6326a51f5c7dd1b/coverage-7.13.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7eda778067ad7ffccd23ecffce537dface96212576a07924cbf0d8799d2ded5a", size = 251716, upload-time = "2026-02-09T12:57:04.056Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/33/50116647905837c66d28b2af1321b845d5f5d19be9655cb84d4a0ea806b4/coverage-7.13.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e87f6c587c3f34356c3759f0420693e35e7eb0e2e41e4c011cb6ec6ecbbf1db7", size = 253089, upload-time = "2026-02-09T12:57:05.503Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/b4/8efb11a46e3665d92635a56e4f2d4529de6d33f2cb38afd47d779d15fc99/coverage-7.13.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:8248977c2e33aecb2ced42fef99f2d319e9904a36e55a8a68b69207fb7e43edc", size = 251232, upload-time = "2026-02-09T12:57:06.879Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/24/8cd73dd399b812cc76bb0ac260e671c4163093441847ffe058ac9fda1e32/coverage-7.13.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:25381386e80ae727608e662474db537d4df1ecd42379b5ba33c84633a2b36d47", size = 255299, upload-time = "2026-02-09T12:57:08.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/94/0a4b12f1d0e029ce1ccc1c800944a9984cbe7d678e470bb6d3c6bc38a0da/coverage-7.13.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:ee756f00726693e5ba94d6df2bdfd64d4852d23b09bb0bc700e3b30e6f333985", size = 250796, upload-time = "2026-02-09T12:57:10.142Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/73/44/6002fbf88f6698ca034360ce474c406be6d5a985b3fdb3401128031eef6b/coverage-7.13.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fdfc1e28e7c7cdce44985b3043bc13bbd9c747520f94a4d7164af8260b3d91f0", size = 252673, upload-time = "2026-02-09T12:57:12.197Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/de/c6/a0279f7c00e786be75a749a5674e6fa267bcbd8209cd10c9a450c655dfa7/coverage-7.13.4-cp312-cp312-win32.whl", hash = "sha256:01d4cbc3c283a17fc1e42d614a119f7f438eabb593391283adca8dc86eff1246", size = 221990, upload-time = "2026-02-09T12:57:14.085Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/4e/c0a25a425fcf5557d9abd18419c95b63922e897bc86c1f327f155ef234a9/coverage-7.13.4-cp312-cp312-win_amd64.whl", hash = "sha256:9401ebc7ef522f01d01d45532c68c5ac40fb27113019b6b7d8b208f6e9baa126", size = 222800, upload-time = "2026-02-09T12:57:15.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/47/ac/92da44ad9a6f4e3a7debd178949d6f3769bedca33830ce9b1dcdab589a37/coverage-7.13.4-cp312-cp312-win_arm64.whl", hash = "sha256:b1ec7b6b6e93255f952e27ab58fbc68dcc468844b16ecbee881aeb29b6ab4d8d", size = 221415, upload-time = "2026-02-09T12:57:17.497Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/4a/331fe2caf6799d591109bb9c08083080f6de90a823695d412a935622abb2/coverage-7.13.4-py3-none-any.whl", hash = "sha256:1af1641e57cf7ba1bd67d677c9abdbcd6cc2ab7da3bca7fa1e2b7e50e65f2ad0", size = 211242, upload-time = "2026-02-09T12:59:02.032Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "46.0.3"
|
||||
|
|
@ -327,6 +351,13 @@ dependencies = [
|
|||
{ name = "sqlmodel" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
dev = [
|
||||
{ name = "pytest" },
|
||||
{ name = "pytest-asyncio" },
|
||||
{ name = "pytest-cov" },
|
||||
]
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "alembic", specifier = ">=1.15.2" },
|
||||
|
|
@ -352,6 +383,9 @@ requires-dist = [
|
|||
{ name = "pydantic-i18n", specifier = ">=0.4.5" },
|
||||
{ name = "pydash", specifier = ">=8.0.5" },
|
||||
{ name = "pyjwt", specifier = ">=2.10.1" },
|
||||
{ name = "pytest", marker = "extra == 'dev'", specifier = ">=8.3.0" },
|
||||
{ name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.24.0" },
|
||||
{ name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=5.0.0" },
|
||||
{ name = "python-dotenv", specifier = ">=1.1.0" },
|
||||
{ name = "python-multipart", specifier = ">=0.0.20" },
|
||||
{ name = "requests", specifier = ">=2.32.4" },
|
||||
|
|
@ -359,6 +393,7 @@ requires-dist = [
|
|||
{ name = "sqlalchemy-utils", specifier = ">=0.41.2" },
|
||||
{ name = "sqlmodel", specifier = ">=0.0.24" },
|
||||
]
|
||||
provides-extras = ["dev"]
|
||||
|
||||
[[package]]
|
||||
name = "email-validator"
|
||||
|
|
@ -470,6 +505,7 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/f8/0a/a3871375c7b9727edaeeea994bfff7c63ff7804c9829c19309ba2e058807/greenlet-3.3.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:b01548f6e0b9e9784a2c99c5651e5dc89ffcbe870bc5fb2e5ef864e9cc6b5dcb", size = 276379, upload-time = "2025-12-04T14:23:30.498Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/43/ab/7ebfe34dce8b87be0d11dae91acbf76f7b8246bf9d6b319c741f99fa59c6/greenlet-3.3.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:349345b770dc88f81506c6861d22a6ccd422207829d2c854ae2af8025af303e3", size = 597294, upload-time = "2025-12-04T14:50:06.847Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a4/39/f1c8da50024feecd0793dbd5e08f526809b8ab5609224a2da40aad3a7641/greenlet-3.3.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e8e18ed6995e9e2c0b4ed264d2cf89260ab3ac7e13555b8032b25a74c6d18655", size = 607742, upload-time = "2025-12-04T14:57:42.349Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/77/cb/43692bcd5f7a0da6ec0ec6d58ee7cddb606d055ce94a62ac9b1aa481e969/greenlet-3.3.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c024b1e5696626890038e34f76140ed1daf858e37496d33f2af57f06189e70d7", size = 622297, upload-time = "2025-12-04T15:07:13.552Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/75/b0/6bde0b1011a60782108c01de5913c588cf51a839174538d266de15e4bf4d/greenlet-3.3.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:047ab3df20ede6a57c35c14bf5200fcf04039d50f908270d3f9a7a82064f543b", size = 609885, upload-time = "2025-12-04T14:26:02.368Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/49/0e/49b46ac39f931f59f987b7cd9f34bfec8ef81d2a1e6e00682f55be5de9f4/greenlet-3.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2d9ad37fc657b1102ec880e637cccf20191581f75c64087a549e66c57e1ceb53", size = 1567424, upload-time = "2025-12-04T15:04:23.757Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/05/f5/49a9ac2dff7f10091935def9165c90236d8f175afb27cbed38fb1d61ab6b/greenlet-3.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:83cd0e36932e0e7f36a64b732a6f60c2fc2df28c351bae79fbaf4f8092fe7614", size = 1636017, upload-time = "2025-12-04T14:27:29.688Z" },
|
||||
|
|
@ -531,6 +567,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "itsdangerous"
|
||||
version = "2.2.0"
|
||||
|
|
@ -698,6 +743,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "packaging"
|
||||
version = "26.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pandas"
|
||||
version = "3.0.0"
|
||||
|
|
@ -752,6 +806,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/5d/cf/881b457eccacac9e5b2ddd97d5071fb6d668307c57cbf4e3b5278e06e536/pillow-12.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:65b80c1ee7e14a87d6a068dd3b0aea268ffcabfe0498d38661b00c5b4b22e74c", size = 2452612, upload-time = "2026-01-02T09:11:29.309Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pluggy"
|
||||
version = "1.6.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psutil"
|
||||
version = "5.9.8"
|
||||
|
|
@ -881,6 +944,15 @@ wheels = [
|
|||
{ url = "https://files.pythonhosted.org/packages/a5/b7/cc5e7974699db40014d58c7dd7c4ad4ffc244d36930dc9ec7d06ee67d7a9/pydash-8.0.6-py3-none-any.whl", hash = "sha256:ee70a81a5b292c007f28f03a4ee8e75c1f5d7576df5457b836ec7ab2839cc5d0", size = 101561, upload-time = "2026-01-17T16:42:55.448Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pygments"
|
||||
version = "2.19.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pyjwt"
|
||||
version = "2.10.1"
|
||||
|
|
@ -895,6 +967,49 @@ crypto = [
|
|||
{ name = "cryptography" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest"
|
||||
version = "9.0.2"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||
{ name = "iniconfig" },
|
||||
{ name = "packaging" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pygments" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-asyncio"
|
||||
version = "1.3.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pytest" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pytest-cov"
|
||||
version = "7.0.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "coverage" },
|
||||
{ name = "pluggy" },
|
||||
{ name = "pytest" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "python-dateutil"
|
||||
version = "2.9.0.post0"
|
||||
|
|
|
|||
|
|
@ -29,9 +29,14 @@ import {
|
|||
UploadCloud,
|
||||
X,
|
||||
} from 'lucide-react';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { toast } from 'sonner';
|
||||
import {
|
||||
BUILTIN_AGENTS,
|
||||
MentionAgent,
|
||||
MentionDropdown,
|
||||
} from './MentionDropdown';
|
||||
|
||||
/**
|
||||
* File attachment object
|
||||
|
|
@ -71,6 +76,10 @@ export interface InputboxProps {
|
|||
privacy?: boolean;
|
||||
/** Use cloud model in dev */
|
||||
useCloudModelInDev?: boolean;
|
||||
/** Active @mention target (e.g. "browser") — shown as a tag in the input */
|
||||
mentionTarget?: string | null;
|
||||
/** Callback when mention target changes */
|
||||
onMentionTargetChange?: (target: string | null) => void;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -126,6 +135,8 @@ export const Inputbox = ({
|
|||
allowDragDrop = false,
|
||||
privacy = true,
|
||||
useCloudModelInDev = false,
|
||||
mentionTarget,
|
||||
onMentionTargetChange,
|
||||
}: InputboxProps) => {
|
||||
const { t } = useTranslation();
|
||||
const internalTextareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
|
|
@ -137,6 +148,11 @@ export const Inputbox = ({
|
|||
const [isRemainingOpen, setIsRemainingOpen] = useState(false);
|
||||
const hoverCloseTimerRef = useRef<number | null>(null);
|
||||
const [isComposing, setIsComposing] = useState(false);
|
||||
const [mentionState, setMentionState] = useState<{
|
||||
visible: boolean;
|
||||
filter: string;
|
||||
startIndex: number;
|
||||
}>({ visible: false, filter: '', startIndex: -1 });
|
||||
|
||||
const openRemainingPopover = () => {
|
||||
if (hoverCloseTimerRef.current) {
|
||||
|
|
@ -168,8 +184,56 @@ export const Inputbox = ({
|
|||
const hasContent = value.trim().length > 0 || files.length > 0;
|
||||
const isActive = isFocused || hasContent;
|
||||
|
||||
const handleTextChange = (newValue: string) => {
|
||||
const handleTextChange = useCallback(
|
||||
(newValue: string, cursorPos?: number) => {
|
||||
onChange?.(newValue);
|
||||
|
||||
// Detect @ mention
|
||||
const pos = cursorPos ?? newValue.length;
|
||||
const textBeforeCursor = newValue.slice(0, pos);
|
||||
const lastAtIndex = textBeforeCursor.lastIndexOf('@');
|
||||
|
||||
if (
|
||||
lastAtIndex >= 0 &&
|
||||
(lastAtIndex === 0 ||
|
||||
textBeforeCursor[lastAtIndex - 1] === ' ' ||
|
||||
textBeforeCursor[lastAtIndex - 1] === '\n')
|
||||
) {
|
||||
const filterText = textBeforeCursor.slice(lastAtIndex + 1);
|
||||
if (!filterText.includes(' ')) {
|
||||
setMentionState({
|
||||
visible: true,
|
||||
filter: filterText,
|
||||
startIndex: lastAtIndex,
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
setMentionState({ visible: false, filter: '', startIndex: -1 });
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
|
||||
const handleMentionSelect = (agent: MentionAgent) => {
|
||||
// Remove the "@filter" text from the input and set the
|
||||
// mention target as a rendered tag instead
|
||||
const currentValue = value;
|
||||
const before = currentValue.slice(0, mentionState.startIndex);
|
||||
const afterFilterEnd =
|
||||
mentionState.startIndex + 1 + mentionState.filter.length;
|
||||
const after = currentValue.slice(afterFilterEnd);
|
||||
const newValue = `${before}${after}`.trimStart();
|
||||
onChange?.(newValue);
|
||||
onMentionTargetChange?.(agent.id);
|
||||
setMentionState({ visible: false, filter: '', startIndex: -1 });
|
||||
|
||||
// Focus textarea
|
||||
requestAnimationFrame(() => {
|
||||
const el = textareaRef.current;
|
||||
if (el) {
|
||||
el.focus();
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const handleSend = () => {
|
||||
|
|
@ -183,6 +247,23 @@ export const Inputbox = ({
|
|||
};
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
||||
// When mention dropdown is open, let it handle navigation keys
|
||||
if (mentionState.visible) {
|
||||
if (['ArrowUp', 'ArrowDown', 'Tab', 'Escape', 'Enter'].includes(e.key)) {
|
||||
e.preventDefault(); // Stop textarea from scrolling / inserting newline
|
||||
return; // Let MentionDropdown's global handler handle these
|
||||
}
|
||||
}
|
||||
// Backspace at cursor position 0 removes the mention tag
|
||||
if (
|
||||
e.key === 'Backspace' &&
|
||||
mentionTarget &&
|
||||
textareaRef.current?.selectionStart === 0 &&
|
||||
textareaRef.current?.selectionEnd === 0
|
||||
) {
|
||||
e.preventDefault();
|
||||
onMentionTargetChange?.(null);
|
||||
}
|
||||
if (e.key === 'Enter' && !e.shiftKey && !disabled && !isComposing) {
|
||||
e.preventDefault();
|
||||
handleSend();
|
||||
|
|
@ -289,15 +370,43 @@ export const Inputbox = ({
|
|||
<div className="text-sm font-semibold">Drop files to attach</div>
|
||||
</div>
|
||||
)}
|
||||
{/* @Mention Dropdown */}
|
||||
<MentionDropdown
|
||||
visible={mentionState.visible}
|
||||
filter={mentionState.filter}
|
||||
onSelect={handleMentionSelect}
|
||||
onClose={() =>
|
||||
setMentionState({ visible: false, filter: '', startIndex: -1 })
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Text Input Area */}
|
||||
<div className="relative box-border flex w-full items-center justify-center gap-2.5 px-0 pb-2 pt-2.5">
|
||||
<div className="relative mx-2 box-border flex min-h-px min-w-px flex-1 items-center justify-center gap-2.5 py-0">
|
||||
<div className="relative mx-2 box-border flex min-h-px min-w-px flex-1 items-center gap-2 py-0">
|
||||
{/* @Mention Tag */}
|
||||
{mentionTarget && (
|
||||
<span
|
||||
className="bg-info-primary/15 text-info-primary hover:bg-info-primary/25 inline-flex shrink-0 cursor-pointer items-center gap-1 rounded-md px-2 py-0.5 text-xs font-semibold transition-colors"
|
||||
onClick={() => onMentionTargetChange?.(null)}
|
||||
title="Click to remove"
|
||||
>
|
||||
@
|
||||
{BUILTIN_AGENTS.find((a) => a.id === mentionTarget)?.label ??
|
||||
mentionTarget}
|
||||
<X size={12} className="opacity-60" />
|
||||
</span>
|
||||
)}
|
||||
<Textarea
|
||||
variant="none"
|
||||
size="default"
|
||||
ref={textareaRef}
|
||||
value={value}
|
||||
onChange={(e) => handleTextChange(e.target.value)}
|
||||
onChange={(e) =>
|
||||
handleTextChange(
|
||||
e.target.value,
|
||||
e.target.selectionStart ?? undefined
|
||||
)
|
||||
}
|
||||
onKeyDown={handleKeyDown}
|
||||
onCompositionStart={() => setIsComposing(true)}
|
||||
onCompositionEnd={() => setIsComposing(false)}
|
||||
|
|
|
|||
156
src/components/ChatBox/BottomBox/MentionDropdown.tsx
Normal file
156
src/components/ChatBox/BottomBox/MentionDropdown.tsx
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
// ========= Copyright 2025-2026 @ Eigent.ai All Rights Reserved. =========
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
|
||||
export interface MentionAgent {
|
||||
id: string;
|
||||
label: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const BUILTIN_AGENTS: MentionAgent[] = [
|
||||
{
|
||||
id: 'workforce',
|
||||
label: 'Workforce',
|
||||
description: 'Task decomposition & multi-agent collaboration',
|
||||
},
|
||||
{
|
||||
id: 'browser',
|
||||
label: 'Browser Agent',
|
||||
description: 'Browser automation',
|
||||
},
|
||||
{
|
||||
id: 'dev',
|
||||
label: 'Developer Agent',
|
||||
description: 'Terminal & code execution',
|
||||
},
|
||||
{
|
||||
id: 'doc',
|
||||
label: 'Document Agent',
|
||||
description: 'Document processing',
|
||||
},
|
||||
{
|
||||
id: 'media',
|
||||
label: 'Multi Modal Agent',
|
||||
description: 'Image & video analysis',
|
||||
},
|
||||
];
|
||||
|
||||
interface MentionDropdownProps {
|
||||
visible: boolean;
|
||||
filter: string;
|
||||
onSelect: (agent: MentionAgent) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const MentionDropdown = ({
|
||||
visible,
|
||||
filter,
|
||||
onSelect,
|
||||
onClose,
|
||||
}: MentionDropdownProps) => {
|
||||
const [selectedIndex, setSelectedIndex] = useState(0);
|
||||
const listRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const filteredAgents = BUILTIN_AGENTS.filter(
|
||||
(agent) =>
|
||||
agent.id.toLowerCase().includes(filter.toLowerCase()) ||
|
||||
agent.label.toLowerCase().includes(filter.toLowerCase())
|
||||
);
|
||||
|
||||
// Reset selection when filter changes
|
||||
useEffect(() => {
|
||||
setSelectedIndex(0);
|
||||
}, [filter]);
|
||||
|
||||
// Keyboard navigation
|
||||
useEffect(() => {
|
||||
if (!visible) return;
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'ArrowDown') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setSelectedIndex((prev) =>
|
||||
prev < filteredAgents.length - 1 ? prev + 1 : 0
|
||||
);
|
||||
} else if (e.key === 'ArrowUp') {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
setSelectedIndex((prev) =>
|
||||
prev > 0 ? prev - 1 : filteredAgents.length - 1
|
||||
);
|
||||
} else if (e.key === 'Enter' || e.key === 'Tab') {
|
||||
if (filteredAgents.length > 0) {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
onSelect(filteredAgents[selectedIndex]);
|
||||
}
|
||||
} else if (e.key === 'Escape') {
|
||||
e.preventDefault();
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener('keydown', handleKeyDown, true);
|
||||
return () => document.removeEventListener('keydown', handleKeyDown, true);
|
||||
}, [visible, filteredAgents, selectedIndex, onSelect, onClose]);
|
||||
|
||||
// Scroll selected item into view
|
||||
useEffect(() => {
|
||||
if (!visible || !listRef.current) return;
|
||||
const item = listRef.current.children[selectedIndex] as HTMLElement;
|
||||
item?.scrollIntoView({ block: 'nearest' });
|
||||
}, [selectedIndex, visible]);
|
||||
|
||||
if (!visible || filteredAgents.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={listRef}
|
||||
className={cn(
|
||||
'absolute bottom-full left-0 z-50 mb-1 w-64',
|
||||
'rounded-lg border border-dropdown-border bg-dropdown-bg shadow-perfect',
|
||||
'max-h-[240px] overflow-auto py-1'
|
||||
)}
|
||||
>
|
||||
{filteredAgents.map((agent, index) => (
|
||||
<div
|
||||
key={agent.id}
|
||||
className={cn(
|
||||
'flex cursor-pointer flex-col gap-0.5 px-3 py-2 transition-colors',
|
||||
index === selectedIndex
|
||||
? 'bg-dropdown-item-bg-hover'
|
||||
: 'hover:bg-dropdown-item-bg-hover'
|
||||
)}
|
||||
onMouseEnter={() => setSelectedIndex(index)}
|
||||
onMouseDown={(e) => {
|
||||
e.preventDefault(); // Prevent textarea blur
|
||||
onSelect(agent);
|
||||
}}
|
||||
>
|
||||
<span className="text-sm font-medium text-text-body">
|
||||
@{agent.id}
|
||||
<span className="ml-2 text-xs font-normal text-text-label">
|
||||
{agent.label}
|
||||
</span>
|
||||
</span>
|
||||
<span className="text-xs text-text-label">{agent.description}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -24,12 +24,13 @@ const COPIED_RESET_MS = 2000;
|
|||
|
||||
const SKILL_TAG_REGEX = /\{\{([^}]+)\}\}/g;
|
||||
|
||||
function parseContentWithSkillTags(
|
||||
content: string
|
||||
): Array<{ type: 'text'; value: string } | { type: 'skill'; name: string }> {
|
||||
const nodes: Array<
|
||||
{ type: 'text'; value: string } | { type: 'skill'; name: string }
|
||||
> = [];
|
||||
type ContentNode =
|
||||
| { type: 'text'; value: string }
|
||||
| { type: 'skill'; name: string }
|
||||
| { type: 'mention'; id: string };
|
||||
|
||||
function parseContentWithTags(content: string): ContentNode[] {
|
||||
const nodes: ContentNode[] = [];
|
||||
let lastIndex = 0;
|
||||
let m: RegExpExecArray | null;
|
||||
SKILL_TAG_REGEX.lastIndex = 0;
|
||||
|
|
@ -37,7 +38,13 @@ function parseContentWithSkillTags(
|
|||
if (m.index > lastIndex) {
|
||||
nodes.push({ type: 'text', value: content.slice(lastIndex, m.index) });
|
||||
}
|
||||
nodes.push({ type: 'skill', name: m[1].trim() });
|
||||
const inner = m[1].trim();
|
||||
if (inner.startsWith('@')) {
|
||||
// {{@browser}} → mention tag
|
||||
nodes.push({ type: 'mention', id: inner.slice(1) });
|
||||
} else {
|
||||
nodes.push({ type: 'skill', name: inner });
|
||||
}
|
||||
lastIndex = m.index + m[0].length;
|
||||
}
|
||||
if (lastIndex < content.length) {
|
||||
|
|
@ -46,6 +53,14 @@ function parseContentWithSkillTags(
|
|||
return nodes.length > 0 ? nodes : [{ type: 'text', value: content }];
|
||||
}
|
||||
|
||||
const MENTION_LABELS: Record<string, string> = {
|
||||
workforce: 'Workforce',
|
||||
browser: 'Browser Agent',
|
||||
dev: 'Developer Agent',
|
||||
doc: 'Document Agent',
|
||||
media: 'Multi Modal Agent',
|
||||
};
|
||||
|
||||
interface UserMessageCardProps {
|
||||
id: string;
|
||||
content: string;
|
||||
|
|
@ -107,8 +122,10 @@ export function UserMessageCard({
|
|||
window.electronAPI?.openSkillFolder?.(skillName);
|
||||
};
|
||||
|
||||
const contentNodes = parseContentWithSkillTags(content);
|
||||
const hasSkillTags = contentNodes.some((n) => n.type === 'skill');
|
||||
const contentNodes = parseContentWithTags(content);
|
||||
const hasSpecialTags = contentNodes.some(
|
||||
(n) => n.type === 'skill' || n.type === 'mention'
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -125,11 +142,23 @@ export function UserMessageCard({
|
|||
</Button>
|
||||
</div>
|
||||
<div className="whitespace-pre-wrap break-words text-body-sm text-text-body">
|
||||
{hasSkillTags
|
||||
? contentNodes.map((node, i) =>
|
||||
node.type === 'text' ? (
|
||||
<span key={i}>{node.value}</span>
|
||||
) : (
|
||||
{hasSpecialTags
|
||||
? contentNodes.map((node, i) => {
|
||||
if (node.type === 'text') {
|
||||
return <span key={i}>{node.value}</span>;
|
||||
}
|
||||
if (node.type === 'mention') {
|
||||
return (
|
||||
<span
|
||||
key={i}
|
||||
className="bg-info-primary/15 text-info-primary mr-1.5 inline-flex items-center rounded-md px-1.5 py-0.5 align-middle text-xs font-semibold"
|
||||
>
|
||||
@{MENTION_LABELS[node.id] ?? node.id}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
// skill
|
||||
return (
|
||||
<button
|
||||
key={i}
|
||||
type="button"
|
||||
|
|
@ -143,8 +172,8 @@ export function UserMessageCard({
|
|||
<Sparkles className="h-3.5 w-3.5 text-icon-primary" />
|
||||
{node.name}
|
||||
</button>
|
||||
)
|
||||
)
|
||||
);
|
||||
})
|
||||
: content}
|
||||
</div>
|
||||
{attaches && attaches.length > 0 && (
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ import { ProjectChatContainer } from './ProjectChatContainer';
|
|||
|
||||
export default function ChatBox(): JSX.Element {
|
||||
const [message, setMessage] = useState<string>('');
|
||||
const [mentionTarget, setMentionTarget] = useState<string | null>(null);
|
||||
|
||||
//Get Chatstore for the active project's task
|
||||
const { chatStore, projectStore } = useChatStoreAdapter();
|
||||
|
|
@ -261,8 +262,10 @@ export default function ChatBox(): JSX.Element {
|
|||
task.messages.some(
|
||||
(m) => m.step === AgentStep.TO_SUB_TASKS && !m.isConfirm
|
||||
) ||
|
||||
// skeleton/computing phase
|
||||
(!task.messages.find((m) => m.step === AgentStep.TO_SUB_TASKS) &&
|
||||
// skeleton/computing phase (not applicable after task finishes or in direct agent mode)
|
||||
((task.status as string) !== ChatTaskStatus.FINISHED &&
|
||||
(task.status as string) !== ChatTaskStatus.RUNNING &&
|
||||
!task.messages.find((m) => m.step === AgentStep.TO_SUB_TASKS) &&
|
||||
!task.hasWaitComfirm &&
|
||||
task.messages.length > 0) ||
|
||||
task.isTakeControl
|
||||
|
|
@ -430,7 +433,31 @@ export default function ChatBox(): JSX.Element {
|
|||
return;
|
||||
}
|
||||
|
||||
const tempMessageContent = messageStr || message;
|
||||
const rawMessageContent = messageStr || message;
|
||||
|
||||
// Use the active mentionTarget state (rendered as a tag in the input).
|
||||
// Fall back to parsing @mention from text for backwards compat.
|
||||
let activeMentionTarget = mentionTarget;
|
||||
let tempMessageContent = rawMessageContent;
|
||||
if (!activeMentionTarget) {
|
||||
const mentionMatch = rawMessageContent.match(/^@(\w+)\s+([\s\S]*)/);
|
||||
if (mentionMatch) {
|
||||
activeMentionTarget = mentionMatch[1];
|
||||
tempMessageContent = mentionMatch[2];
|
||||
}
|
||||
}
|
||||
|
||||
// Build display content: embed mention as {{@agentId}} so it
|
||||
// survives in the message text and gets rendered like skill tags.
|
||||
const displayContent = activeMentionTarget
|
||||
? `{{@${activeMentionTarget}}} ${tempMessageContent}`
|
||||
: tempMessageContent;
|
||||
|
||||
// Persist the mention target so the tag stays for the next turn
|
||||
if (activeMentionTarget && activeMentionTarget !== mentionTarget) {
|
||||
setMentionTarget(activeMentionTarget);
|
||||
}
|
||||
|
||||
chatStore.setHasMessages(_taskId as string, true);
|
||||
if (!_taskId) return;
|
||||
|
||||
|
|
@ -447,7 +474,8 @@ export default function ChatBox(): JSX.Element {
|
|||
) ||
|
||||
(!task.messages.find((m) => m.step === AgentStep.TO_SUB_TASKS) &&
|
||||
!task.hasWaitComfirm &&
|
||||
task.messages.length > 0) ||
|
||||
task.messages.length > 0 &&
|
||||
task.status !== ChatTaskStatus.FINISHED) ||
|
||||
task.isTakeControl ||
|
||||
// explicit confirm wait while task is pending but card not confirmed yet
|
||||
(!!task.messages.find(
|
||||
|
|
@ -471,7 +499,7 @@ export default function ChatBox(): JSX.Element {
|
|||
chatStore.addMessages(_taskId, {
|
||||
id: generateUniqueId(),
|
||||
role: 'user',
|
||||
content: tempMessageContent,
|
||||
content: displayContent,
|
||||
attaches:
|
||||
JSON.parse(JSON.stringify(chatStore.tasks[_taskId]?.attaches)) ||
|
||||
[],
|
||||
|
|
@ -568,7 +596,9 @@ export default function ChatBox(): JSX.Element {
|
|||
undefined,
|
||||
undefined,
|
||||
tempMessageContent,
|
||||
attachesToSend
|
||||
attachesToSend,
|
||||
activeMentionTarget ?? undefined,
|
||||
displayContent
|
||||
);
|
||||
chatStore.setAttaches(_taskId, []);
|
||||
} catch (err: any) {
|
||||
|
|
@ -603,12 +633,13 @@ export default function ChatBox(): JSX.Element {
|
|||
question: tempMessageContent,
|
||||
task_id: nextTaskId,
|
||||
attaches: improveAttaches,
|
||||
target: activeMentionTarget,
|
||||
});
|
||||
chatStore.setIsPending(_taskId, true);
|
||||
chatStore.addMessages(_taskId, {
|
||||
id: generateUniqueId(),
|
||||
role: 'user',
|
||||
content: tempMessageContent,
|
||||
content: displayContent,
|
||||
attaches: attachesForThisTurn,
|
||||
});
|
||||
chatStore.setAttaches(_taskId, []);
|
||||
|
|
@ -648,7 +679,9 @@ export default function ChatBox(): JSX.Element {
|
|||
undefined,
|
||||
undefined,
|
||||
tempMessageContent,
|
||||
attachesToSend
|
||||
attachesToSend,
|
||||
activeMentionTarget ?? undefined,
|
||||
displayContent
|
||||
);
|
||||
chatStore.setHasWaitComfirm(_taskId as string, true);
|
||||
chatStore.setAttaches(_taskId, []);
|
||||
|
|
@ -895,8 +928,10 @@ export default function ChatBox(): JSX.Element {
|
|||
|
||||
// Determine if we're in the "splitting in progress" phase (skeleton visible)
|
||||
// Only show splitting if there's NO to_sub_tasks message yet (not even confirmed)
|
||||
// Skip splitting phase when task is already RUNNING (e.g. direct @agent mode)
|
||||
const isSkeletonPhase =
|
||||
(task.status !== ChatTaskStatus.FINISHED &&
|
||||
task.status !== ChatTaskStatus.RUNNING &&
|
||||
!anyToSubTasksMessage &&
|
||||
!task.hasWaitComfirm &&
|
||||
task.messages.length > 0) ||
|
||||
|
|
@ -1078,6 +1113,8 @@ export default function ChatBox(): JSX.Element {
|
|||
allowDragDrop: true,
|
||||
privacy: privacy,
|
||||
useCloudModelInDev: useCloudModelInDev,
|
||||
mentionTarget: mentionTarget,
|
||||
onMentionTargetChange: setMentionTarget,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -1122,6 +1159,8 @@ export default function ChatBox(): JSX.Element {
|
|||
allowDragDrop: true,
|
||||
privacy: privacy,
|
||||
useCloudModelInDev: useCloudModelInDev,
|
||||
mentionTarget: mentionTarget,
|
||||
onMentionTargetChange: setMentionTarget,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -94,7 +94,9 @@ export interface ChatStore {
|
|||
shareToken?: string,
|
||||
delayTime?: number,
|
||||
messageContent?: string,
|
||||
messageAttaches?: File[]
|
||||
messageAttaches?: File[],
|
||||
target?: string,
|
||||
displayContent?: string
|
||||
) => Promise<void>;
|
||||
handleConfirmTask: (
|
||||
project_id: string,
|
||||
|
|
@ -417,7 +419,9 @@ const chatStore = (initial?: Partial<ChatStore>) =>
|
|||
shareToken?: string,
|
||||
delayTime?: number,
|
||||
messageContent?: string,
|
||||
messageAttaches?: File[]
|
||||
messageAttaches?: File[],
|
||||
target?: string,
|
||||
displayContent?: string
|
||||
) => {
|
||||
// ✅ Wait for backend to be ready before starting task (except for replay/share)
|
||||
if (!type || type === 'normal') {
|
||||
|
|
@ -480,7 +484,7 @@ const chatStore = (initial?: Partial<ChatStore>) =>
|
|||
targetChatStore.getState().addMessages(newTaskId, {
|
||||
id: generateUniqueId(),
|
||||
role: 'user',
|
||||
content: messageContent,
|
||||
content: displayContent || messageContent,
|
||||
attaches: messageAttaches || [],
|
||||
});
|
||||
targetChatStore.getState().setHasMessages(newTaskId, true);
|
||||
|
|
@ -732,6 +736,7 @@ const chatStore = (initial?: Partial<ChatStore>) =>
|
|||
cdp_browsers: cdp_browsers,
|
||||
env_path: envPath,
|
||||
search_config: searchConfig,
|
||||
target: target || null,
|
||||
})
|
||||
: undefined,
|
||||
|
||||
|
|
@ -816,8 +821,22 @@ const chatStore = (initial?: Partial<ChatStore>) =>
|
|||
const shouldCreateNewChat =
|
||||
project_id && (question || messageContent);
|
||||
|
||||
// Direct agent follow-up: reuse existing chatStore
|
||||
// (continuous chat — don't create a new chatStore)
|
||||
const isDirectFollowUp =
|
||||
agentMessages.data?.direct === true && !skipFirstConfirm;
|
||||
if (isDirectFollowUp) {
|
||||
// Just reset status to RUNNING so the UI unblocks
|
||||
// and SSE events are not filtered out
|
||||
previousChatStore.setStatus(
|
||||
currentTaskId,
|
||||
ChatTaskStatus.RUNNING
|
||||
);
|
||||
previousChatStore.setIsPending(currentTaskId, false);
|
||||
}
|
||||
|
||||
//All except first confirmed event to reuse the existing chatStore
|
||||
if (shouldCreateNewChat && !skipFirstConfirm) {
|
||||
if (shouldCreateNewChat && !skipFirstConfirm && !isDirectFollowUp) {
|
||||
/**
|
||||
* For Tasks where appended to existing project by
|
||||
* reusing same projectId. Need to create new chatStore
|
||||
|
|
@ -837,6 +856,13 @@ const chatStore = (initial?: Partial<ChatStore>) =>
|
|||
updateLockedReferences(newChatStore, newTaskId);
|
||||
newChatStore.getState().setIsPending(newTaskId, false);
|
||||
|
||||
// direct=true means @agent mode: skip task splitting
|
||||
if (agentMessages.data?.direct === true) {
|
||||
newChatStore
|
||||
.getState()
|
||||
.setStatus(newTaskId, ChatTaskStatus.RUNNING);
|
||||
}
|
||||
|
||||
if (type === 'replay') {
|
||||
newChatStore
|
||||
.getState()
|
||||
|
|
@ -911,9 +937,12 @@ const chatStore = (initial?: Partial<ChatStore>) =>
|
|||
} else {
|
||||
//NOTE: Triggered only with first "confirmed" in the project
|
||||
//Handle Original cases - with old chatStore
|
||||
// direct=true means @agent mode: skip task splitting,
|
||||
// go straight to RUNNING
|
||||
const directAgent = agentMessages.data?.direct === true;
|
||||
previousChatStore.setStatus(
|
||||
currentTaskId,
|
||||
ChatTaskStatus.PENDING
|
||||
directAgent ? ChatTaskStatus.RUNNING : ChatTaskStatus.PENDING
|
||||
);
|
||||
previousChatStore.setHasWaitComfirm(currentTaskId, false);
|
||||
}
|
||||
|
|
@ -961,6 +990,8 @@ const chatStore = (initial?: Partial<ChatStore>) =>
|
|||
setStreamingDecomposeText,
|
||||
clearStreamingDecomposeText,
|
||||
setIsTaskEdit,
|
||||
setActiveAgent,
|
||||
setActiveWorkspace: _setActiveWorkspace,
|
||||
} = getCurrentChatStore();
|
||||
|
||||
currentTaskId = getCurrentTaskId();
|
||||
|
|
@ -1380,6 +1411,34 @@ const chatStore = (initial?: Partial<ChatStore>) =>
|
|||
status: AgentMessageStatus.RUNNING,
|
||||
});
|
||||
}
|
||||
|
||||
// Direct agent mode: create synthetic task if agent
|
||||
// has no tasks assigned (no workforce ASSIGN_TASK)
|
||||
const isDirect =
|
||||
taskAssigning[agentIndex].tasks.length === 0 && process_task_id;
|
||||
if (isDirect) {
|
||||
const syntheticTaskBase = {
|
||||
id: process_task_id,
|
||||
content: agentMessages.data.message || '',
|
||||
status: TaskStatus.RUNNING,
|
||||
agent: {
|
||||
agent_id: agent_id,
|
||||
status: AgentStatusValue.RUNNING,
|
||||
},
|
||||
};
|
||||
// Push separate copies so they don't share
|
||||
// the same toolkits array reference
|
||||
taskAssigning[agentIndex].tasks.push({
|
||||
...syntheticTaskBase,
|
||||
toolkits: [],
|
||||
} as any);
|
||||
taskRunning.push({ ...syntheticTaskBase, toolkits: [] } as any);
|
||||
// Activate agent in sidebar but stay on main page
|
||||
// (don't setActiveWorkspace — that would switch to
|
||||
// the browser/developer full-screen workspace panel)
|
||||
setActiveAgent(currentTaskId, agent_id!);
|
||||
}
|
||||
|
||||
const taskIndex = taskRunning.findIndex(
|
||||
(task) => task.id === process_task_id
|
||||
);
|
||||
|
|
@ -1416,8 +1475,18 @@ const chatStore = (initial?: Partial<ChatStore>) =>
|
|||
);
|
||||
if (taskIndex !== -1 && taskRunning[taskIndex].agent) {
|
||||
taskRunning[taskIndex].agent!.status = 'completed';
|
||||
taskRunning[taskIndex].status = TaskStatus.COMPLETED;
|
||||
}
|
||||
|
||||
// Also update taskAssigning task status
|
||||
const assignedTask = taskAssigning[agentIndex].tasks.find(
|
||||
(task: TaskInfo) => task.id === process_task_id
|
||||
);
|
||||
if (assignedTask) {
|
||||
assignedTask.status = TaskStatus.COMPLETED;
|
||||
}
|
||||
taskAssigning[agentIndex].status = AgentStatusValue.COMPLETED;
|
||||
|
||||
if (!type && historyId) {
|
||||
const obj = {
|
||||
project_name: tasks[currentTaskId].summaryTask.split('|')[0],
|
||||
|
|
@ -2253,6 +2322,7 @@ const chatStore = (initial?: Partial<ChatStore>) =>
|
|||
task.id === agentMessages.data.process_task_id
|
||||
)
|
||||
);
|
||||
if (assigneeAgentIndex === -1) return;
|
||||
const task = taskAssigning[assigneeAgentIndex].tasks.find(
|
||||
(task: TaskInfo) =>
|
||||
task.id === agentMessages.data.process_task_id
|
||||
|
|
|
|||
1
src/types/chatbox.d.ts
vendored
1
src/types/chatbox.d.ts
vendored
|
|
@ -146,6 +146,7 @@ declare global {
|
|||
current_length?: number;
|
||||
max_length?: number;
|
||||
text?: string;
|
||||
direct?: boolean;
|
||||
};
|
||||
status?: AgentMessageStatusType;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue