feat(page-ai): add skill context and agent profile policy
This commit is contained in:
@@ -68,6 +68,7 @@ pub struct AcpMnoteToolContext {
|
||||
pub trace_id: Option<String>,
|
||||
pub workspace_id: Option<String>,
|
||||
pub document_id: Option<String>,
|
||||
pub mnote_capabilities: Option<Value>,
|
||||
}
|
||||
|
||||
/// Handler for session events.
|
||||
@@ -567,6 +568,7 @@ impl AcpSessionManager {
|
||||
trace_id: mnote_context.trace_id,
|
||||
workspace_id: mnote_context.workspace_id,
|
||||
document_id: mnote_context.document_id,
|
||||
mnote_capabilities: mnote_context.mnote_capabilities,
|
||||
};
|
||||
|
||||
debug!("ACP session/prompt (session={})", session_id);
|
||||
|
||||
@@ -193,6 +193,8 @@ pub struct SessionPromptParams {
|
||||
pub workspace_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub document_id: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub mnote_capabilities: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
use crate::app::AppState;
|
||||
use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use crate::hermes_tools::{doc, ToolCallInput};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
pub async fn context_snapshot(
|
||||
_state: &AppState,
|
||||
_context: &RequestContext,
|
||||
input: &ToolCallInput,
|
||||
) -> Result<Value, WebError> {
|
||||
let context_refs = input
|
||||
.arg_value("contextRefs")
|
||||
.or_else(|| input.arg_value("context_refs"))
|
||||
.unwrap_or_else(|| json!([]));
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"schema": "mnote.context_snapshot.v1",
|
||||
"runId": input.run_id,
|
||||
"sessionId": input.session_id,
|
||||
"workspace": {
|
||||
"workspaceId": input.effective_workspace_id(),
|
||||
"sourceKind": input.effective_source_kind(),
|
||||
"rootUri": input.effective_root_uri()
|
||||
},
|
||||
"contextRefs": context_refs,
|
||||
"primaryTarget": {
|
||||
"documentId": input.effective_document_id(),
|
||||
"fileVersion": input.arg_value("fileVersion").or_else(|| input.arg_value("file_version"))
|
||||
},
|
||||
"availableReads": {
|
||||
"currentPage": context_ref_enabled(input, "current_page"),
|
||||
"selection": context_ref_enabled(input, "selection"),
|
||||
"folder": context_ref_enabled(input, "folder"),
|
||||
"changedFiles": context_ref_enabled(input, "changed_files")
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn resolve_target(
|
||||
_state: &AppState,
|
||||
_context: &RequestContext,
|
||||
input: &ToolCallInput,
|
||||
) -> Result<Value, WebError> {
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"schema": "mnote.context_target.v1",
|
||||
"workspaceId": input.effective_workspace_id(),
|
||||
"documentId": input.effective_document_id(),
|
||||
"sourceKind": input.effective_source_kind(),
|
||||
"rootUri": input.effective_root_uri(),
|
||||
"relativePath": input.arg_string("relativePath").or_else(|| input.arg_string("relative_path")),
|
||||
"fileVersion": input.arg_value("fileVersion").or_else(|| input.arg_value("file_version")),
|
||||
"contextRefs": input.arg_value("contextRefs").or_else(|| input.arg_value("context_refs")).unwrap_or_else(|| json!([]))
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn read_current_page(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
input: &ToolCallInput,
|
||||
) -> Result<Value, WebError> {
|
||||
if !context_ref_enabled(input, "current_page") {
|
||||
return Err(WebError::new(
|
||||
axum::http::StatusCode::FORBIDDEN,
|
||||
"mnote_context_current_page_not_allowed",
|
||||
"本次 run 未授权 current_page 上下文",
|
||||
)
|
||||
.with_context(context));
|
||||
}
|
||||
doc::doc_fetch(state, context, input).await
|
||||
}
|
||||
|
||||
fn context_ref_enabled(input: &ToolCallInput, expected: &str) -> bool {
|
||||
input
|
||||
.arg_value("contextRefs")
|
||||
.or_else(|| input.arg_value("context_refs"))
|
||||
.and_then(|value| value.as_array().cloned())
|
||||
.map(|items| {
|
||||
items.iter().any(|item| {
|
||||
item.as_str() == Some(expected)
|
||||
|| item.get("kind").and_then(Value::as_str).map(str::trim) == Some(expected)
|
||||
})
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn input_with_refs(context_refs: Value) -> ToolCallInput {
|
||||
ToolCallInput {
|
||||
tool_name: "mnote.context.read_current_page".into(),
|
||||
workspace_id: Some("ws_1".into()),
|
||||
document_id: Some("doc_1".into()),
|
||||
source_kind: Some("local_folder".into()),
|
||||
root_uri: Some("file:///tmp/mnote".into()),
|
||||
actor_id: Some("user_1".into()),
|
||||
profile: None,
|
||||
session_id: Some("sess_1".into()),
|
||||
run_id: Some("run_1".into()),
|
||||
tool_call_id: Some("tool_1".into()),
|
||||
trace_id: Some("trace_1".into()),
|
||||
idempotency_key: None,
|
||||
dry_run: None,
|
||||
capability_scope: Some(vec!["context.read".into()]),
|
||||
args: Some(json!({ "contextRefs": context_refs })),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn context_ref_enabled_accepts_string_and_object_refs() {
|
||||
assert!(context_ref_enabled(
|
||||
&input_with_refs(json!(["current_page"])),
|
||||
"current_page"
|
||||
));
|
||||
assert!(context_ref_enabled(
|
||||
&input_with_refs(json!([{ "kind": "current_page" }])),
|
||||
"current_page"
|
||||
));
|
||||
assert!(!context_ref_enabled(
|
||||
&input_with_refs(json!(["selection"])),
|
||||
"current_page"
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,10 @@ pub fn manifest() -> Value {
|
||||
"writeOwner": "rust-runtime-kernel"
|
||||
},
|
||||
"tools": [
|
||||
skill_read_tool(),
|
||||
context_snapshot_tool(),
|
||||
context_read_current_page_tool(),
|
||||
context_resolve_target_tool(),
|
||||
doc_fetch_tool(),
|
||||
doc_find_tool(),
|
||||
block_fetch_tool(),
|
||||
@@ -37,6 +41,95 @@ pub fn manifest() -> Value {
|
||||
})
|
||||
}
|
||||
|
||||
fn skill_read_tool() -> Value {
|
||||
let mut properties = base_identity_properties();
|
||||
if let Value::Object(map) = &mut properties {
|
||||
map.insert("skillId".into(), json!({ "type": "string" }));
|
||||
map.insert("agentId".into(), json!({ "type": "string" }));
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.skill.read",
|
||||
"description": "按需读取 MNote skill 正文。默认 prompt 只列 skill 摘要,正文必须通过本工具懒加载。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["skill.read"],
|
||||
"status": "available",
|
||||
"annotations": tool_annotations(true, false, true, false),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["skillId"],
|
||||
"properties": properties
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn context_snapshot_tool() -> Value {
|
||||
let mut properties = base_identity_properties();
|
||||
if let Value::Object(map) = &mut properties {
|
||||
map.insert(
|
||||
"contextRefs".into(),
|
||||
json!({ "type": "array", "items": { "type": ["string", "object"] } }),
|
||||
);
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.context.snapshot",
|
||||
"description": "返回本次 run 可用的 MNote 上下文摘要,不返回页面正文全文。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["context.read"],
|
||||
"status": "available",
|
||||
"annotations": tool_annotations(true, false, true, false),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": properties
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn context_read_current_page_tool() -> Value {
|
||||
let mut tool = doc_fetch_tool();
|
||||
if let Value::Object(map) = &mut tool {
|
||||
map.insert(
|
||||
"name".into(),
|
||||
Value::String("mnote.context.read_current_page".into()),
|
||||
);
|
||||
map.insert(
|
||||
"description".into(),
|
||||
Value::String("在 current_page contextRef 被授权时读取当前 Markdown 页面。".into()),
|
||||
);
|
||||
map.insert(
|
||||
"capabilityScope".into(),
|
||||
json!(["context.read", "page.read"]),
|
||||
);
|
||||
}
|
||||
tool
|
||||
}
|
||||
|
||||
fn context_resolve_target_tool() -> Value {
|
||||
let mut properties = base_identity_properties();
|
||||
if let Value::Object(map) = &mut properties {
|
||||
map.insert(
|
||||
"contextRefs".into(),
|
||||
json!({ "type": "array", "items": { "type": ["string", "object"] } }),
|
||||
);
|
||||
map.insert("relativePath".into(), json!({ "type": "string" }));
|
||||
map.insert(
|
||||
"fileVersion".into(),
|
||||
json!({ "type": ["string", "object"] }),
|
||||
);
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.context.resolve_target",
|
||||
"description": "解析当前 Page AI run 的工作区、文档、rootUri、relativePath 与 file version。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["context.read"],
|
||||
"status": "available",
|
||||
"annotations": tool_annotations(true, false, true, false),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": properties
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn base_identity_properties() -> Value {
|
||||
json!({
|
||||
"workspaceId": { "type": "string" },
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
pub mod artifact;
|
||||
pub mod block;
|
||||
pub mod context_tools;
|
||||
pub mod doc;
|
||||
pub mod manifest;
|
||||
pub mod page;
|
||||
pub mod resource;
|
||||
pub mod skill;
|
||||
|
||||
use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use crate::hermes_tools::ToolCallInput;
|
||||
use axum::http::StatusCode;
|
||||
use serde_json::{json, Value};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct MnoteSkill {
|
||||
pub id: &'static str,
|
||||
pub title: &'static str,
|
||||
pub description: &'static str,
|
||||
pub agent_ids: &'static [&'static str],
|
||||
pub read_only: bool,
|
||||
pub requires_context_refs: &'static [&'static str],
|
||||
pub tool_names: &'static [&'static str],
|
||||
pub content: &'static str,
|
||||
}
|
||||
|
||||
const SKILLS: &[MnoteSkill] = &[
|
||||
MnoteSkill {
|
||||
id: "mnote-current-page",
|
||||
title: "MNote current page",
|
||||
description: "Read the current MNote Markdown page only when the task needs page content.",
|
||||
agent_ids: &["hermes", "reasonix"],
|
||||
read_only: true,
|
||||
requires_context_refs: &["current_page"],
|
||||
tool_names: &[
|
||||
"mnote.context.snapshot",
|
||||
"mnote.context.resolve_target",
|
||||
"mnote.context.read_current_page",
|
||||
],
|
||||
content: include_str!("../../../../../skills/mnote-current-page/SKILL.md"),
|
||||
},
|
||||
MnoteSkill {
|
||||
id: "mnote-local-file",
|
||||
title: "MNote local file editing",
|
||||
description: "Read and patch local Markdown files inside MNote allowed roots.",
|
||||
agent_ids: &["hermes", "reasonix"],
|
||||
read_only: false,
|
||||
requires_context_refs: &["current_page", "file", "folder"],
|
||||
tool_names: &["mnote.context.snapshot", "mnote.context.resolve_target"],
|
||||
content: include_str!("../../../../../skills/mnote-local-file/SKILL.md"),
|
||||
},
|
||||
MnoteSkill {
|
||||
id: "mnote-chat-only",
|
||||
title: "MNote chat only",
|
||||
description: "Reply conversationally without reading or writing MNote page/file context.",
|
||||
agent_ids: &["chat_only", "hermes", "reasonix"],
|
||||
read_only: true,
|
||||
requires_context_refs: &[],
|
||||
tool_names: &[],
|
||||
content: include_str!("../../../../../skills/mnote-chat-only/SKILL.md"),
|
||||
},
|
||||
];
|
||||
|
||||
pub fn skill_summaries_for_agent(agent_id: Option<&str>) -> Vec<Value> {
|
||||
SKILLS
|
||||
.iter()
|
||||
.filter(|skill| skill_matches_agent(skill, agent_id))
|
||||
.map(skill_summary)
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn find_skill(skill_id: &str, agent_id: Option<&str>) -> Option<&'static MnoteSkill> {
|
||||
let requested = skill_id.trim();
|
||||
SKILLS
|
||||
.iter()
|
||||
.find(|skill| skill.id == requested && skill_matches_agent(skill, agent_id))
|
||||
}
|
||||
|
||||
pub async fn skill_read(
|
||||
context: &RequestContext,
|
||||
input: &ToolCallInput,
|
||||
) -> Result<Value, WebError> {
|
||||
let skill_id = input
|
||||
.arg_string("skillId")
|
||||
.or_else(|| input.arg_string("skill_id"))
|
||||
.ok_or_else(|| {
|
||||
WebError::bad_request_code("mnote_skill_id_required", "缺少 skillId")
|
||||
.with_context(context)
|
||||
})?;
|
||||
let agent_id = input
|
||||
.arg_string("agentId")
|
||||
.or_else(|| input.arg_string("agent_id"));
|
||||
let skill = find_skill(&skill_id, agent_id.as_deref()).ok_or_else(|| {
|
||||
WebError::new(
|
||||
StatusCode::NOT_FOUND,
|
||||
"mnote_skill_not_found",
|
||||
"未知或当前 agent 不可用的 MNote skill",
|
||||
)
|
||||
.with_context(context)
|
||||
})?;
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"schema": "mnote.skill.v1",
|
||||
"skill": skill_summary(skill),
|
||||
"content": skill.content,
|
||||
"tools": skill.tool_names,
|
||||
"constraints": {
|
||||
"allowedRootsRequired": skill.requires_context_refs.contains(&"folder"),
|
||||
"mustReadBackAfterWrite": !skill.read_only
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn skill_matches_agent(skill: &MnoteSkill, agent_id: Option<&str>) -> bool {
|
||||
let Some(agent_id) = agent_id.map(str::trim).filter(|value| !value.is_empty()) else {
|
||||
return true;
|
||||
};
|
||||
skill
|
||||
.agent_ids
|
||||
.iter()
|
||||
.any(|candidate| *candidate == agent_id)
|
||||
}
|
||||
|
||||
fn skill_summary(skill: &MnoteSkill) -> Value {
|
||||
json!({
|
||||
"id": skill.id,
|
||||
"title": skill.title,
|
||||
"description": skill.description,
|
||||
"agentIds": skill.agent_ids,
|
||||
"readOnly": skill.read_only,
|
||||
"requiresContextRefs": skill.requires_context_refs,
|
||||
"toolNames": skill.tool_names
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn skill_registry_filters_by_agent() {
|
||||
let chat_skills = skill_summaries_for_agent(Some("chat_only"));
|
||||
assert!(chat_skills
|
||||
.iter()
|
||||
.any(|skill| skill["id"] == "mnote-chat-only"));
|
||||
assert!(!chat_skills
|
||||
.iter()
|
||||
.any(|skill| skill["id"] == "mnote-local-file"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skill_lookup_rejects_unknown_or_agent_mismatch() {
|
||||
assert!(find_skill("mnote-current-page", Some("reasonix")).is_some());
|
||||
assert!(find_skill("mnote-local-file", Some("chat_only")).is_none());
|
||||
assert!(find_skill("missing", Some("reasonix")).is_none());
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,7 +2,9 @@ use super::hermes_client;
|
||||
use crate::app::AppState;
|
||||
use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use crate::hermes_tools::{artifact, block, doc, manifest, page, resource, ToolCallInput};
|
||||
use crate::hermes_tools::{
|
||||
artifact, block, context_tools, doc, manifest, page, resource, skill, ToolCallInput,
|
||||
};
|
||||
use axum::extract::{Extension, Query, State};
|
||||
use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
|
||||
use axum::Json;
|
||||
@@ -347,6 +349,14 @@ pub(crate) async fn execute_mnote_tool_call(
|
||||
return Ok(cached);
|
||||
}
|
||||
let result = match input.tool_name.as_str() {
|
||||
"mnote.skill.read" => skill::skill_read(&context, &input).await,
|
||||
"mnote.context.snapshot" => context_tools::context_snapshot(&state, &context, &input).await,
|
||||
"mnote.context.read_current_page" => {
|
||||
context_tools::read_current_page(&state, &context, &input).await
|
||||
}
|
||||
"mnote.context.resolve_target" => {
|
||||
context_tools::resolve_target(&state, &context, &input).await
|
||||
}
|
||||
"mnote.doc.fetch" => doc::doc_fetch(&state, &context, &input).await,
|
||||
"mnote.doc.find" => doc::doc_find(&state, &context, &input).await,
|
||||
"mnote.doc.plan_update" => doc::plan_update(&state, &context, &input).await,
|
||||
@@ -538,6 +548,10 @@ fn is_read_tool(tool_name: &str) -> bool {
|
||||
matches!(
|
||||
tool_name,
|
||||
"mnote.page.get"
|
||||
| "mnote.skill.read"
|
||||
| "mnote.context.snapshot"
|
||||
| "mnote.context.read_current_page"
|
||||
| "mnote.context.resolve_target"
|
||||
| "mnote.doc.fetch"
|
||||
| "mnote.doc.find"
|
||||
| "mnote.block.fetch"
|
||||
@@ -1359,6 +1373,80 @@ mod tests {
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_tools_context_read_current_page_reads_local_markdown() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
"mnote-context-read-current-page-{}",
|
||||
std::process::id()
|
||||
));
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
fs::create_dir_all(&root).expect("root");
|
||||
fs::write(
|
||||
root.join("README.md"),
|
||||
"# 当前页\n\n来自 mnote.context.read_current_page 的正文。\n",
|
||||
)
|
||||
.expect("markdown");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
crate::routes::local_folder_source::initialize_local_workspace_for_actor(
|
||||
"user_1", &root_uri,
|
||||
)
|
||||
.expect("workspace");
|
||||
|
||||
let response = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.method("POST")
|
||||
.uri("/api/hermes/tools/mnote/call")
|
||||
.header("content-type", "application/json")
|
||||
.header("x-mnote-actor-id", "user_1")
|
||||
.header("x-mnote-actor-type", "user")
|
||||
.body(Body::from(
|
||||
json!({
|
||||
"toolName": "mnote.context.read_current_page",
|
||||
"workspaceId": "local-ws-context",
|
||||
"documentId": "local-md:README.md",
|
||||
"sourceKind": "local_folder",
|
||||
"rootUri": root_uri,
|
||||
"profile": "reasonix",
|
||||
"actorId": "user_1",
|
||||
"sessionId": "sess_context_read",
|
||||
"runId": "run_context_read",
|
||||
"toolCallId": "call_context_read",
|
||||
"traceId": "trace_context_read",
|
||||
"args": {
|
||||
"format": "markdown",
|
||||
"contextRefs": [{ "kind": "current_page" }],
|
||||
"aiAccessScope": {
|
||||
"permissionLevel": "read_write",
|
||||
"allowedRoots": [{ "rootUri": root_uri, "permission": "write" }],
|
||||
"allowedResourceIds": ["local-md:README.md"]
|
||||
}
|
||||
}
|
||||
})
|
||||
.to_string(),
|
||||
))
|
||||
.expect("request"),
|
||||
)
|
||||
.await
|
||||
.expect("response");
|
||||
let status = response.status();
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(status, StatusCode::OK, "{payload}");
|
||||
assert_eq!(payload["ok"], true);
|
||||
assert_eq!(payload["result"]["ok"], true);
|
||||
assert!(
|
||||
payload["result"]["content"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.contains("mnote.context.read_current_page"),
|
||||
"{payload}"
|
||||
);
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_tools_mindmap_fetch_reads_authorized_local_resource() {
|
||||
let root = std::env::temp_dir().join(format!(
|
||||
|
||||
@@ -3791,6 +3791,9 @@ body {
|
||||
.wolai-page-ai-icon {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
@@ -3800,6 +3803,13 @@ body {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wolai-page-ai-icon-svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
display: block;
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.wolai-page-ai-icon:hover,
|
||||
.wolai-page-ai-icon.is-active {
|
||||
background: #F3F3F2;
|
||||
@@ -3878,7 +3888,8 @@ body {
|
||||
|
||||
.wolai-page-ai-profile-select,
|
||||
.wolai-page-ai-context-select,
|
||||
.wolai-page-ai-skill-search {
|
||||
.wolai-page-ai-skill-search,
|
||||
.wolai-page-ai-agent-profile {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
min-width: 0;
|
||||
@@ -3886,7 +3897,8 @@ body {
|
||||
|
||||
.wolai-page-ai-profile-select select,
|
||||
.wolai-page-ai-context-select select,
|
||||
.wolai-page-ai-skill-search input {
|
||||
.wolai-page-ai-skill-search input,
|
||||
.wolai-page-ai-agent-profile select {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 34px;
|
||||
@@ -3898,6 +3910,10 @@ body {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.wolai-page-ai-agent-profile {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.wolai-page-ai-inline-error {
|
||||
padding: 8px 10px;
|
||||
border: 1px solid rgba(198, 80, 80, 0.18);
|
||||
@@ -3943,23 +3959,15 @@ body {
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.wolai-page-ai-session-button {
|
||||
.wolai-page-ai-session-summary {
|
||||
min-width: 0;
|
||||
max-width: 190px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: #5A5A5A;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wolai-page-ai-session-button:hover {
|
||||
color: #1B1C1C;
|
||||
}
|
||||
|
||||
.wolai-page-ai-settings-head {
|
||||
@@ -4171,10 +4179,29 @@ body {
|
||||
}
|
||||
|
||||
.wolai-page-ai-skills-toolbar {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) minmax(124px, 160px) max-content;
|
||||
align-items: end;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.wolai-page-ai-skill-filter-toggle {
|
||||
display: inline-flex;
|
||||
min-height: 34px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #5A5A5A;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wolai-page-ai-skill-filter-toggle input {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.wolai-page-ai-skill-list {
|
||||
display: flex;
|
||||
min-height: 0;
|
||||
@@ -4184,6 +4211,45 @@ body {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.wolai-page-ai-skill-group {
|
||||
display: grid;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.wolai-page-ai-skill-group-head {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
padding: 6px 8px 2px;
|
||||
border: 0;
|
||||
border-radius: 6px;
|
||||
background: transparent;
|
||||
color: #8B8782;
|
||||
font-size: 11px;
|
||||
line-height: 16px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wolai-page-ai-skill-group-head:hover {
|
||||
background: #F7F7F6;
|
||||
color: #5A5A5A;
|
||||
}
|
||||
|
||||
.wolai-page-ai-skill-group-title {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.wolai-page-ai-skill-group-chevron {
|
||||
width: 12px;
|
||||
color: #AAA6A0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.wolai-page-ai-skill-row {
|
||||
display: flex;
|
||||
min-height: 38px;
|
||||
@@ -4385,6 +4451,8 @@ button.wolai-page-ai-message-text {
|
||||
}
|
||||
|
||||
.wolai-page-ai-footer {
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
@@ -4412,16 +4480,16 @@ button.wolai-page-ai-message-text {
|
||||
}
|
||||
|
||||
.wolai-page-ai-composer {
|
||||
position: relative;
|
||||
z-index: 20;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
overflow: visible;
|
||||
border: 1px solid rgba(27, 28, 28, 0.12);
|
||||
border-radius: 10px;
|
||||
background: #FFF;
|
||||
}
|
||||
|
||||
.wolai-page-ai-agent-selector,
|
||||
.wolai-page-ai-context-refs,
|
||||
.wolai-page-ai-allowed-roots {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -4430,6 +4498,123 @@ button.wolai-page-ai-message-text {
|
||||
padding: 6px 8px 0;
|
||||
}
|
||||
|
||||
.wolai-page-ai-agent-picker,
|
||||
.wolai-page-ai-context-picker {
|
||||
position: relative;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.wolai-page-ai-agent-button,
|
||||
.wolai-page-ai-context-button {
|
||||
font-size: 17px;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.wolai-page-ai-agent-button[aria-expanded="true"],
|
||||
.wolai-page-ai-context-button[aria-expanded="true"] {
|
||||
border-color: rgba(27, 28, 28, 0.32);
|
||||
background: #F7F6F4;
|
||||
}
|
||||
|
||||
.wolai-page-ai-agent-popover,
|
||||
.wolai-page-ai-context-popover {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
width: min(320px, calc(100vw - 44px));
|
||||
bottom: calc(100% + 8px);
|
||||
z-index: 30;
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 12px;
|
||||
border: 1px solid rgba(27, 28, 28, 0.12);
|
||||
border-radius: 10px;
|
||||
background: #FFF;
|
||||
box-shadow: 0 16px 36px rgba(27, 28, 28, 0.16);
|
||||
}
|
||||
|
||||
.wolai-page-ai-agent-popover[hidden],
|
||||
.wolai-page-ai-context-popover[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.wolai-page-ai-context-popover-head,
|
||||
.wolai-page-ai-context-popover-foot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
color: #5A5A5A;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.wolai-page-ai-context-option-list {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.wolai-page-ai-agent-option-list {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.wolai-page-ai-agent-option {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid rgba(27, 28, 28, 0.08);
|
||||
border-radius: 8px;
|
||||
background: #FFF;
|
||||
color: #1B1C1C;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.wolai-page-ai-agent-option.is-active {
|
||||
border-color: rgba(27, 28, 28, 0.2);
|
||||
background: #F7F6F4;
|
||||
}
|
||||
|
||||
.wolai-page-ai-agent-option-label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.wolai-page-ai-agent-option-detail {
|
||||
color: #8B8782;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.wolai-page-ai-context-option {
|
||||
display: grid;
|
||||
grid-template-columns: 16px minmax(0, 1fr);
|
||||
gap: 8px;
|
||||
align-items: start;
|
||||
color: #1B1C1C;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.wolai-page-ai-context-option.is-disabled {
|
||||
color: #AAA6A0;
|
||||
}
|
||||
|
||||
.wolai-page-ai-context-option-main {
|
||||
display: grid;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.wolai-page-ai-context-option-label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.wolai-page-ai-context-option-detail {
|
||||
color: #8B8782;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.wolai-page-ai-allowed-roots {
|
||||
padding-top: 4px;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user