Merge pull request #8 from M3gA-Mind/develop

Develop
This commit is contained in:
Cyrus Gray
2026-03-25 20:55:11 +05:30
committed by GitHub
18 changed files with 2035 additions and 231 deletions
@@ -0,0 +1,275 @@
# Skills → Memory Layer → Agent Inference: Full Flow
## Overview
This document traces the complete data flow from skill discovery and OAuth/sync events, through
the TinyHumans Neocortex memory layer (tinyhumansai SDK), and into the Rust-side agentic
inference loop — showing exactly how skill data is written to and read from memory and how it
reaches the LLM context at inference time.
---
## 1. Skill Discovery & Lifecycle (SkillProvider.tsx)
**File**: `src/providers/SkillProvider.tsx`
On app mount (when a JWT token is present) `SkillProvider` calls
`invoke('runtime_discover_skills')` → the Rust runtime scans
`skills/skills/{skill-id}/manifest.json` from the git submodule and returns a manifest list.
```
Token present
→ discoverSkills()
→ invoke('runtime_discover_skills') // Rust: qjs_engine.rs:184
→ reads skills/skills/*/manifest.json
→ filters: is_javascript() && supports_current_platform()
→ returns manifests[]
→ skillManager.registerSkill(manifest) // in-memory registry
→ for each manifest with setupComplete:
skillManager.startSkill(manifest) // starts V8/QuickJS instance
```
Two Tauri event listeners run continuously:
| Event | What it does |
|---|---|
| `skill-state-changed` | Dispatches `setSkillState` into Redux `skillsSlice.skillStates[skillId]` |
| `runtime:skill-status-changed` | Updates `skillsSlice.skills[skillId].status`; surfaces errors |
---
## 2. Memory Client Initialisation
**File**: `src-tauri/src/memory/mod.rs`
**Tauri command**: `init_memory_client` (called by frontend after auth)
The `MemoryClient` wraps the `TinyHumansMemoryClient` from the `tinyhumansai` Rust crate.
It is constructed with the user's JWT (`authSlice.token`) and stored as `Arc<MemoryClient>`
in `MemoryState` (a `Mutex<Option<MemoryClientRef>>`).
Base URL resolution (in priority order):
1. `ALPHAHUMAN_BASE_URL` env var
2. `TINYHUMANS_BASE_URL` env var
3. SDK default
---
## 3. Skill → Memory Sync: Two Write Paths
Both trigger inside `src-tauri/src/runtime/qjs_skill_instance.rs`.
### 3a. OAuth Completion (`skill/oauth-complete`)
When a user connects a skill via OAuth, the JS runtime calls back with `skill/oauth-complete`.
After the OAuth flow completes the skill's **ops state** (data published via `state.set()`) is
snapshotted and stored to memory:
```
skill/oauth-complete handler
→ handle_js_call(rt, ctx, "onOAuthComplete", params) // runs skill JS
→ ops_state.read().data.clone() // snapshot skill state
→ tokio::spawn (fire-and-forget):
MemoryClient::store_skill_sync(
skill_id = e.g. "gmail"
integration_id = params["integrationId"] // e.g. user email
title = "{skill} OAuth sync — {integrationId}"
content = JSON snapshot of ops state
namespace = "skill:{skill_id}:{integration_id}"
)
→ tinyhumansai::TinyHumansMemoryClient::insert_memory(InsertMemoryParams { ... })
→ HTTP POST to TinyHumans Neocortex API
```
### 3b. Periodic Sync (`skill/sync`)
The runtime fires `skill/sync` events on a cron schedule. The flow is identical to OAuth
completion but uses `integration_id = "default"` and title `"{skill} periodic sync"`:
```
skill/sync handler
→ handle_js_call(rt, ctx, "onSync", "{}")
→ ops_state.read().data.clone()
→ tokio::spawn:
MemoryClient::store_skill_sync(
skill_id = e.g. "notion"
integration_id = "default"
namespace = "skill:notion:default"
content = JSON snapshot
)
```
### Namespace Pattern
All skill memories are stored under `skill:{skill_id}:{integration_id}`.
Examples: `skill:gmail:user@example.com`, `skill:notion:default`.
---
## 4. Memory Operations Reference
**File**: `src-tauri/src/memory/mod.rs`
| Method | SDK call | When used |
|---|---|---|
| `store_skill_sync(...)` | `insert_memory` | OAuth complete, periodic sync |
| `query_skill_context(...)` | `query_memory` | RAG query — fetch relevant chunks for a user question |
| `recall_skill_context(...)` | `recall_memory` | Recall synthesised summary from Master node |
| `clear_skill_memory(...)` | `delete_memory` | OAuth revoke / disconnect |
---
## 5. Conversation → Inference: Two Code Paths
**File**: `src/pages/Conversations.tsx`
`useRustChat()` returns `true` when running in Tauri (desktop). This selects between two paths:
```
handleSendMessage(text)
├─ rustChat == true → Rust path (invoke chat_send)
└─ rustChat == false → Web path (handleSendMessageWeb — TypeScript loop)
```
### 5a. Rust Path (Desktop)
```
chatSend({ threadId, message, model, authToken, backendUrl, messages, notionContext })
→ invoke('chat_send') // src-tauri/src/commands/chat.rs:359
→ spawns background task
→ chat_send_inner(...)
```
Completion events flow back over Tauri events:
| Event | Frontend handler |
|---|---|
| `chat:tool_call` | shows active tool indicator |
| `chat:tool_result` | clears tool indicator |
| `chat:done` | `dispatch(addInferenceResponse(...))` |
| `chat:error` | shows error, clears loading state |
### 5b. Web/Fallback Path
Used when not running in Tauri (browser). Runs the agentic loop entirely in TypeScript:
- Calls `inferenceApi.createChatCompletion(request)` directly
- Executes tools via `skillManager.callTool(skillId, toolName, args)`
- Both paths share the same 5-round `MAX_TOOL_ROUNDS` limit and `{skillId}__{toolName}` naming convention
---
## 6. Rust Agentic Loop in Detail
**File**: `src-tauri/src/commands/chat.rs``chat_send_inner()`
```
Step 1: Load OpenClaw context
→ load_openclaw_context(app)
→ reads ai/SOUL.md, IDENTITY.md, AGENTS.md, USER.md, BOOTSTRAP.md, MEMORY.md, TOOLS.md
→ cached in static AI_CONFIG_CACHE (cleared on restart)
→ truncated to MAX_CONTEXT_CHARS (20,000 chars)
Step 2: Recall memory context
→ MemoryClient::recall_skill_context("conversations", thread_id, 10)
→ tinyhumansai recall_memory(namespace="skill:conversations:{thread_id}")
→ returns synthesised summary string or None
Step 3: Build processed user message
processed = user_message
if openclaw_context → prepend as "## Project Context\n...\n\nUser message: {processed}"
if memory_context → prepend as "[MEMORY_CONTEXT]\n{mem}\n[/MEMORY_CONTEXT]\n\n{processed}"
if notion_context → prepend as "{notionContext}\n\n{processed}"
Step 4: Build messages array
→ history (ChatMessagePayload[]) + processed user message
Step 5: Discover tools
→ engine.all_tools()
→ returns all tools from running skills
→ namespaced: "{skill_id}__{tool_name}"
→ formatted as OpenAI function-calling schema
Step 6: Agentic loop (max 5 rounds)
for round in 0..MAX_TOOL_ROUNDS:
POST {backend_url}/openai/v1/chat/completions
body: { model, messages, tools, tool_choice: "auto" }
timeout: 120s
auth: Bearer {auth_token}
if finish_reason == "tool_calls":
emit chat:tool_call
engine.call_tool(skill_id, tool_name, args) // 60s timeout
→ QuickJS/V8 runtime executes skill JS tool handler
emit chat:tool_result
append tool result to messages
continue loop
else (finish_reason == "stop"):
emit chat:done { full_response, rounds_used, token_counts }
return Ok(())
```
---
## 7. Tool Execution: Rust → Skill JS
**File**: `src-tauri/src/runtime/qjs_engine.rs``call_tool(skill_id, tool_name, args)`
The Rust runtime routes `call_tool` into the running QuickJS/V8 skill instance:
- Serialises `args` as JSON
- Calls the JS tool handler registered by the skill
- Returns `ToolCallResult { content: Vec<ToolContent>, is_error: bool }`
- Text content is extracted and appended as the `tool` role message in the loop
---
## 8. End-to-End Flow Summary
```
User types message (Conversations.tsx)
├─ [Web path only] invoke('recall_memory') → TinyHumans API (recall)
│ returns synthesised context
├─ buildNotionContext() → from Redux skillStates.notion
invoke('chat_send') [Rust/desktop path]
Rust: chat_send_inner()
├─ load_openclaw_context() → ai/*.md files (cached)
├─ recall_skill_context("conversations", tid) → TinyHumans API (recall)
├─ Build prompt: [OpenClaw] + [MEMORY] + [Notion] + user_message
├─ discover_tools() → all running skill tools
└─ Agentic loop (≤5 rounds):
POST /openai/v1/chat/completions → Backend LLM (neocortex-mk1)
├─ tool_calls → engine.call_tool() → QuickJS/V8 skill instance
│ └─ JS tool handler
│ └─ returns result string
│ └─ appended as tool message
└─ stop → emit chat:done
└─ Frontend: dispatch(addInferenceResponse(...))
Separately (async, fire-and-forget):
Skill onSync() / onOAuthComplete()
→ MemoryClient::store_skill_sync()
→ TinyHumans insert_memory (namespace: skill:{id}:{integrationId})
```
---
## Key Files Reference
| File | Role |
|---|---|
| `src/providers/SkillProvider.tsx` | Discovery, lifecycle, Redux state sync |
| `src/pages/Conversations.tsx` | Message send, both code paths, Notion context |
| `src/services/chatService.ts` | `chatSend()`, `chatCancel()`, `useRustChat()` |
| `src-tauri/src/commands/chat.rs` | Rust agentic loop, context assembly, tool dispatch |
| `src-tauri/src/commands/memory.rs` | `recall_memory` Tauri command |
| `src-tauri/src/memory/mod.rs` | `MemoryClient` wrapping tinyhumansai SDK |
| `src-tauri/src/runtime/qjs_skill_instance.rs` | Skill sync → memory write triggers |
| `src-tauri/src/runtime/qjs_engine.rs` | `discover_skills()`, `call_tool()`, `all_tools()` |
| `skills/skills/*/manifest.json` | Skill metadata (git submodule) |
+6 -3
View File
@@ -47,12 +47,13 @@
"@scure/bip32": "^2.0.1",
"@scure/bip39": "^2.0.1",
"@sentry/react": "^10.38.0",
"@tauri-apps/api": "2.10.1",
"@tauri-apps/api": "^2.10.0",
"@tauri-apps/plugin-deep-link": "^2",
"@tauri-apps/plugin-opener": "^2",
"@tauri-apps/plugin-os": "^2.3.2",
"@tauri-apps/plugin-shell": "~2",
"@types/react-router-dom": "^5.3.3",
"@types/three": "^0.183.1",
"buffer": "^6.0.3",
"debug": "^4.4.3",
"immer": "^11.1.3",
@@ -69,6 +70,7 @@
"redux-persist": "^6.0.0",
"socket.io-client": "^4.8.3",
"telegram": "^2.26.22",
"three": "^0.183.2",
"util": "^0.12.5",
"zustand": "^5.0.10"
},
@@ -76,7 +78,7 @@
"@eslint/js": "^9.39.2",
"@tailwindcss/forms": "^0.5.11",
"@tailwindcss/typography": "^0.5.19",
"@tauri-apps/cli": "^2",
"@tauri-apps/cli": "2.9.6",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
@@ -112,5 +114,6 @@
"vite": "^7.0.4",
"vite-plugin-node-polyfills": "^0.25.0",
"vitest": "^4.0.18"
}
},
"packageManager": "pnpm@10.27.0+sha512.72d699da16b1179c14ba9e64dc71c9a40988cbdc65c264cb0e489db7de917f20dcf4d64d8723625f2969ba52d4b7e2a1170682d9ac2a5dcaeaab732b7e16f04a"
}
+1 -1
Submodule skills updated: ff2534fc4d...14217d3aee
+3 -2
View File
@@ -8457,10 +8457,11 @@ dependencies = [
[[package]]
name = "tinyhumansai"
version = "0.1.4"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7a88dba7fe194a507d0351c5f8b5cd8518fb30f8307dd788333f9715f51c44e"
checksum = "691a096ecaaeec23ac7bbcfb276058de9d4aff1c574ee074125d5e5080e17b7d"
dependencies = [
"log",
"reqwest 0.12.28",
"serde",
"serde_json",
+1 -1
View File
@@ -140,7 +140,7 @@ opentelemetry_sdk = { version = "0.31", default-features = false, features = ["t
opentelemetry-otlp = { version = "0.31", default-features = false, features = ["trace", "metrics", "http-proto", "reqwest-client", "reqwest-rustls-webpki-roots"] }
tokio-stream = { version = "0.1.18", features = ["full"] }
url = "2"
tinyhumansai = "0.1.4"
tinyhumansai = "0.1.5"
# Optional integrations
matrix-sdk = { version = "0.16", optional = true, default-features = false, features = ["e2e-encryption", "rustls-tls", "markdown"] }
+31 -30
View File
@@ -2,46 +2,47 @@ use std::env;
use std::path::PathBuf;
fn main() {
maybe_override_tauri_config_for_tests();
maybe_override_tauri_config_for_local_builds();
tauri_build::build();
}
fn maybe_override_tauri_config_for_tests() {
fn maybe_override_tauri_config_for_local_builds() {
let profile = env::var("PROFILE").unwrap_or_default();
let skip_resources = env::var("TAURI_SKIP_RESOURCES").is_ok() || profile == "test";
if !skip_resources {
return;
}
let is_release = profile == "release";
let manifest_dir =
PathBuf::from(env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR not set"));
let config_path = manifest_dir.join("tauri.conf.json");
let Ok(raw) = std::fs::read_to_string(&config_path) else {
println!("cargo:warning=Failed to read tauri.conf.json; keeping default config");
let tdlib_framework_path = manifest_dir.join("libraries/libtdjson.1.8.29.dylib");
let skip_missing_frameworks = !is_release && !tdlib_framework_path.exists();
if !skip_resources && !skip_missing_frameworks {
return;
};
let mut value: serde_json::Value = match serde_json::from_str(&raw) {
Ok(value) => value,
Err(err) => {
println!("cargo:warning=Failed to parse tauri.conf.json: {err}");
return;
}
};
if let Some(bundle) = value.get_mut("bundle").and_then(|b| b.as_object_mut()) {
bundle.insert("resources".to_string(), serde_json::Value::Array(Vec::new()));
}
let out_dir = env::var("OUT_DIR").unwrap_or_else(|_| ".".into());
let override_path = PathBuf::from(out_dir).join("tauri.conf.test.json");
if std::fs::write(&override_path, serde_json::to_string_pretty(&value).unwrap_or(raw)).is_ok()
{
env::set_var("TAURI_CONFIG", &override_path);
println!(
"cargo:warning=TAURI resources disabled for test build (using {})",
override_path.display()
);
let mut merge_config = serde_json::json!({});
if skip_resources {
merge_config["bundle"]["resources"] = serde_json::json!([]);
}
if skip_missing_frameworks {
merge_config["bundle"]["macOS"]["frameworks"] = serde_json::json!([]);
}
match serde_json::to_string(&merge_config) {
Ok(json) => {
env::set_var("TAURI_CONFIG", json);
if skip_resources {
println!("cargo:warning=TAURI resources disabled for local build");
}
if skip_missing_frameworks {
println!(
"cargo:warning=TAURI macOS frameworks disabled because {} is missing",
tdlib_framework_path.display()
);
}
}
Err(err) => {
println!("cargo:warning=Failed to serialize TAURI_CONFIG override: {err}");
}
}
}
File diff suppressed because it is too large Load Diff
+2
View File
@@ -1,4 +1,5 @@
pub mod auth;
pub mod chat;
pub mod memory;
pub mod model;
pub mod runtime;
@@ -11,6 +12,7 @@ pub mod window;
// Re-export all commands for registration
pub use auth::*;
pub use chat::{chat_cancel, chat_send};
pub use memory::*;
pub use model::*;
pub use runtime::*;
+12
View File
@@ -23,6 +23,7 @@ mod unified_skills;
mod utils;
use ai::*;
use commands::chat::ChatState;
use commands::unified_skills::{
unified_execute_skill, unified_generate_skill, unified_list_skills, unified_self_evolve_skill,
};
@@ -688,6 +689,11 @@ pub fn run() {
app.manage(commands::memory::MemoryState(std::sync::Mutex::new(None)));
log::info!("[memory] Memory state registered — awaiting JWT from frontend");
// Initialize ChatState for managing in-flight conversation requests
let chat_state = std::sync::Arc::new(ChatState::new());
app.manage(chat_state);
log::info!("[chat] ChatState registered");
// Store SocketManager as Tauri state
app.manage(socket_mgr.clone());
@@ -839,6 +845,9 @@ pub fn run() {
init_memory_client,
memory_query,
recall_memory,
// Chat commands (agentic conversation loop)
chat_send,
chat_cancel,
]
}
#[cfg(not(desktop))]
@@ -960,6 +969,9 @@ pub fn run() {
init_memory_client,
memory_query,
recall_memory,
// Chat commands (agentic conversation loop)
chat_send,
chat_cancel,
]
}
})
+102 -26
View File
@@ -7,14 +7,15 @@
use std::sync::Arc;
use tinyhumansai::{
DeleteMemoryParams, InsertMemoryParams, Priority, QueryMemoryParams, RecallMemoryParams,
SourceType, TinyHumanConfig, TinyHumanMemoryClient,
SourceType, TinyHumanConfig, TinyHumansMemoryClient,
};
use uuid::Uuid;
/// Shared, cloneable handle to the memory client.
pub type MemoryClientRef = Arc<MemoryClient>;
pub struct MemoryClient {
inner: TinyHumanMemoryClient,
inner: TinyHumansMemoryClient,
}
impl MemoryClient {
@@ -38,7 +39,7 @@ impl MemoryClient {
TinyHumanConfig::new(jwt_token)
}
};
match TinyHumanMemoryClient::new(config) {
match TinyHumansMemoryClient::new(config) {
Ok(inner) => {
log::info!("[memory] from_token: exit — client created successfully");
Some(Self { inner })
@@ -52,7 +53,9 @@ impl MemoryClient {
/// Store a skill data-sync result.
///
/// Namespace pattern: `skill:{skill_id}:{integration_id}`
/// Inserts the document then polls `ingestion_job_status` every 30 s until
/// the job reaches `completed` (or `failed`/`error`). Returns only after the
/// ingestion job is confirmed complete.
pub async fn store_skill_sync(
&self,
skill_id: &str,
@@ -66,15 +69,23 @@ impl MemoryClient {
updated_at: Option<f64>,
document_id: Option<String>,
) -> Result<(), String> {
let namespace = format!("skill:{skill_id}:{integration_id}");
log::info!("[memory] store_skill_sync: entry (namespace={namespace}, title={title:?}, content_len={})", content.len());
log::debug!(
"[memory] store_skill_sync: payload → namespace={namespace} | title={title} | content={}",
content
let namespace = skill_id.to_string();
log::info!(
"[memory] store_skill_sync: entry (namespace={namespace}, title={title:?}, content_len={})",
content.len()
);
log::info!("[memory] insert_memory: calling SDK (namespace={namespace}, title={title:?})");
let result = self.inner
let document_id_final = document_id.unwrap_or_else(|| Uuid::new_v4().to_string());
log::info!(
"[memory] insert_memory: calling SDK (namespace={namespace}, title={title:?}), content_len={}",
content.len()
);
let insert_resp = self
.inner
.insert_memory(InsertMemoryParams {
document_id: Some(document_id_final),
title: title.to_string(),
content: content.to_string(),
namespace: namespace.clone(),
@@ -83,33 +94,97 @@ impl MemoryClient {
priority,
created_at,
updated_at,
document_id,
..Default::default()
})
.await
.map(|_| {
log::info!("[memory] insert_memory: success (namespace={namespace}, title={title:?})");
})
.map_err(|e| {
log::warn!("[memory] insert_memory: SDK error — kind={:?} msg={e}", classify_insert_error(&e));
log::warn!(
"[memory] insert_memory: SDK error — kind={:?} msg={e}",
classify_insert_error(&e)
);
format!("Memory insert failed: {e}")
});
match &result {
Ok(()) => log::info!("[memory] store_skill_sync: exit — ok (namespace={namespace})"),
Err(e) => log::warn!("[memory] store_skill_sync: exit — error (namespace={namespace}): {e}"),
})?;
log::info!(
"[memory] insert_memory: accepted (namespace={namespace}, status={:?}, job_id={:?})",
insert_resp.data.status,
insert_resp.data.job_id
);
// If the API returned a job_id, poll until the job completes.
if let Some(job_id) = insert_resp.data.job_id {
log::info!("[memory] ingestion job queued (job_id={job_id}), polling every 30s...");
loop {
tokio::time::sleep(std::time::Duration::from_secs(30)).await;
match self.inner.ingestion_job_status(&job_id).await {
Ok(status_resp) => {
let state = status_resp
.data
.state
.as_deref()
.unwrap_or("unknown");
log::info!(
"[memory] ingestion job status: job_id={job_id}, state={state}, \
attempts={:?}, completed_at={:?}",
status_resp.data.attempts,
status_resp.data.completed_at
);
match state {
"completed" => {
log::info!(
"[memory] ingestion job completed (job_id={job_id}, namespace={namespace})"
);
break;
}
"failed" | "error" => {
let err_msg = status_resp
.data
.error
.unwrap_or_else(|| format!("job state={state}"));
log::warn!(
"[memory] ingestion job failed: job_id={job_id}, error={err_msg}"
);
log::warn!(
"[memory] store_skill_sync: exit — ingestion failed (namespace={namespace})"
);
return Err(format!("Ingestion job failed: {err_msg}"));
}
_ => {
// pending / processing / queued — keep waiting
log::info!(
"[memory] ingestion job still in progress (state={state}), waiting 30s..."
);
}
}
}
Err(e) => {
log::warn!(
"[memory] ingestion job status poll error (job_id={job_id}): {e} — retrying in 30s"
);
}
}
}
} else {
log::info!("[memory] no job_id returned — insert assumed synchronous, proceeding");
}
result
log::info!("[memory] store_skill_sync: exit — ok (namespace={namespace})");
Ok(())
}
/// Query relevant context for a skill integration (RAG).
pub async fn query_skill_context(
&self,
skill_id: &str,
integration_id: &str,
_integration_id: &str,
query: &str,
max_chunks: u32,
) -> Result<String, String> {
let namespace = format!("skill:{skill_id}:{integration_id}");
let namespace = skill_id.to_string();
log::info!("[memory] query_skill_context: entry (namespace={namespace}, max_chunks={max_chunks}, query={query:?})");
log::debug!(
"[memory] query_skill_context: payload → namespace={namespace} | max_chunks={max_chunks} | query={query}"
@@ -137,10 +212,10 @@ impl MemoryClient {
pub async fn recall_skill_context(
&self,
skill_id: &str,
integration_id: &str,
_integration_id: &str,
max_chunks: u32,
) -> Result<Option<String>, String> {
let namespace = format!("skill:{skill_id}:{integration_id}");
let namespace = skill_id.to_string();
log::info!(
"[memory] recall_skill_context: entry (namespace={namespace}, max_chunks={max_chunks})"
);
@@ -187,7 +262,7 @@ impl MemoryClient {
}
}
fn classify_insert_error(e: &tinyhumansai::TinyHumanError) -> &'static str {
fn classify_insert_error(e: &tinyhumansai::TinyHumansError) -> &'static str {
let msg = e.to_string();
if msg.contains("dns") || msg.contains("resolve") || msg.contains("lookup") {
"dns_failure"
@@ -253,6 +328,7 @@ mod tests {
None,
None,
None,
None,
)
.await;
+1 -44
View File
@@ -583,50 +583,7 @@ async fn handle_message(
}).await;
log::info!("[skill:{}] OAuth credential set and persisted to store", skill_id);
let params_str = serde_json::to_string(&params).unwrap_or_else(|_| "{}".to_string());
let result = handle_js_call(rt, ctx, "onOAuthComplete", &params_str).await;
// Fire-and-forget: persist published ops state to TinyHumans memory.
// Skills publish data via state.set()/setPartial() into ops_state.data,
// not as the return value of onOAuthComplete() (which is typically undefined).
let state_snapshot = ops_state.read().data.clone();
if !state_snapshot.is_empty() {
if let Some(client) = memory_client_opt.clone() {
let skill = skill_id.to_string();
let integration_id = params
.get("integrationId")
.and_then(|v| v.as_str())
.unwrap_or("unknown")
.to_string();
let content = serde_json::to_string_pretty(
&serde_json::Value::Object(state_snapshot),
)
.unwrap_or_else(|_| "{}".to_string());
let title = format!("{} OAuth sync — {}", skill, integration_id);
tokio::spawn(async move {
if let Err(e) = client
.store_skill_sync(
&skill,
&integration_id,
&title,
&content,
None,
None,
None,
None,
None,
None,
)
.await
{
log::warn!("[memory] store_skill_sync failed: {e}");
} else {
log::info!("[memory] Stored sync for {}:{}", skill, integration_id);
}
});
}
}
result
handle_js_call(rt, ctx, "onOAuthComplete", &params_str).await
}
"skill/ping" => {
handle_js_call(rt, ctx, "onPing", "{}").await
+2 -1
View File
@@ -38,7 +38,8 @@
"icons/icon.ico"
],
"resources": [
"../skills/skills"
"../skills/skills",
"../ai"
],
"macOS": {
"minimumSystemVersion": "10.15",
+1 -1
View File
@@ -8,11 +8,11 @@ import Conversations from './pages/Conversations';
import Home from './pages/Home';
import Intelligence from './pages/Intelligence';
import Invites from './pages/Invites';
import Skills from './pages/Skills';
import Login from './pages/Login';
import Mnemonic from './pages/Mnemonic';
import Onboarding from './pages/onboarding/Onboarding';
import Settings from './pages/Settings';
import Skills from './pages/Skills';
import Welcome from './pages/Welcome';
import { selectHasEncryptionKey, selectIsOnboarded } from './store/authSelectors';
import { useAppSelector } from './store/hooks';
+219 -48
View File
@@ -18,8 +18,18 @@ import {
type ModelInfo,
type Tool,
} from '../services/api/inferenceApi';
import {
chatCancel,
chatSend,
subscribeChatEvents,
useRustChat,
type ChatToolCallEvent,
type ChatToolResultEvent,
} from '../services/chatService';
import { useAppDispatch, useAppSelector } from '../store/hooks';
import type { NotionPageSummary, NotionSummary, NotionUserProfile } from '../store/notionSlice';
import { store } from '../store';
import { BACKEND_URL } from '../utils/config';
import {
addInferenceResponse,
addMessageLocal,
@@ -141,6 +151,17 @@ const Conversations = () => {
const [isLoadingModels, setIsLoadingModels] = useState(false);
const [isSending, setIsSending] = useState(false);
const [sendError, setSendError] = useState<string | null>(null);
const [activeToolCall, setActiveToolCall] = useState<{ name: string; args: unknown } | null>(
null
);
const rustChat = useRustChat();
// Ref to track selectedThreadId inside event callbacks without re-subscribing
const selectedThreadIdRef = useRef(selectedThreadId);
useEffect(() => {
selectedThreadIdRef.current = selectedThreadId;
}, [selectedThreadId]);
// Budget state
const [teamUsage, setTeamUsage] = useState<TeamUsage | null>(null);
const [isLoadingBudget, setIsLoadingBudget] = useState(false);
@@ -288,6 +309,78 @@ const Conversations = () => {
}
}, [inputValue, sendError]);
// Subscribe to Rust chat events when running in Tauri.
// Registered ONCE (deps: [rustChat]) — uses refs for values that change.
useEffect(() => {
if (!rustChat) return;
let cleanup: (() => void) | null = null;
let mounted = true;
subscribeChatEvents({
onToolCall: (event: ChatToolCallEvent) => {
if (event.thread_id !== selectedThreadIdRef.current) return;
setActiveToolCall({ name: event.tool_name, args: event.args });
},
onToolResult: (event: ChatToolResultEvent) => {
if (event.thread_id !== selectedThreadIdRef.current) return;
setActiveToolCall(null);
},
onDone: event => {
// Guard against duplicate dispatch (React StrictMode double-fires effects in dev)
const currentState = store.getState() as {
thread: { messagesByThreadId: Record<string, ThreadMessage[]> };
};
const threadMessages = currentState.thread.messagesByThreadId[event.thread_id] || [];
const lastMsg = threadMessages[threadMessages.length - 1];
if (lastMsg?.sender === 'agent' && lastMsg?.content === event.full_response) {
return; // Already added — skip duplicate
}
dispatch(addInferenceResponse({ content: event.full_response, threadId: event.thread_id }));
setIsSending(false);
setActiveToolCall(null);
dispatch(setActiveThread(null));
},
onError: event => {
if (event.thread_id !== selectedThreadIdRef.current) return;
if (event.error_type !== 'cancelled') {
setSendError(event.message);
}
setIsSending(false);
setActiveToolCall(null);
dispatch(setActiveThread(null));
// Remove the optimistic user message on error
dispatch((innerDispatch, getState) => {
const state = getState() as {
thread: { messagesByThreadId: Record<string, ThreadMessage[]> };
};
const persistedMessages = state.thread.messagesByThreadId[event.thread_id] || [];
const lastUserIdx = [...persistedMessages]
.reverse()
.findIndex(m => m.sender === 'user');
if (lastUserIdx !== -1) {
const actualIdx = persistedMessages.length - 1 - lastUserIdx;
const updated = persistedMessages.filter((_, i) => i !== actualIdx);
innerDispatch(updateMessagesForThread({ threadId: event.thread_id, messages: updated }));
if (event.thread_id === selectedThreadIdRef.current) {
innerDispatch(setSelectedThread(event.thread_id));
}
}
});
},
}).then(fn => {
if (mounted) cleanup = fn;
});
return () => {
mounted = false;
cleanup?.();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [rustChat]);
const handleSelectThread = (threadId: string) => {
if (threadId === selectedThreadId) return;
navigate(`/conversations/${threadId}`, { replace: true });
@@ -321,49 +414,14 @@ const Conversations = () => {
}
};
const handleSendMessage = async (text?: string) => {
const trimmed = text ?? inputValue.trim();
if (!trimmed || !selectedThreadId || isSending) return;
// Check if another thread is already sending
if (activeThreadId && activeThreadId !== selectedThreadId) {
return; // Block sending from non-active threads
}
// Store the original thread ID to ensure response goes to correct thread
const sendingThreadId = selectedThreadId;
// Create stable user message and persist immediately
const userMessage: ThreadMessage = {
id: `msg_${Date.now()}_${Math.random()}`,
content: trimmed,
type: 'text',
extraMetadata: {},
sender: 'user',
createdAt: new Date().toISOString(),
};
// Immediately persist user message to both current view and persistent storage
dispatch(addMessageLocal({ threadId: sendingThreadId, message: userMessage }));
// Update current view if this is the selected thread
if (sendingThreadId === selectedThreadId) {
// Message is already added to persistent storage, reload current view
dispatch(setSelectedThread(sendingThreadId));
}
// Snapshot history for AI request (excluding the just-added user message since we'll add it manually)
const historySnapshot = messages.filter(
m => !m.id.startsWith('optimistic-') && m.id !== userMessage.id
);
setInputValue('');
setSendError(null);
setIsSending(true);
// Set this thread as active
dispatch(setActiveThread(sendingThreadId));
// Web fallback: the original orchestration logic, preserved exactly as-is.
// Called when not running in Tauri.
const handleSendMessageWeb = async (
sendingThreadId: string,
trimmed: string,
userMessage: ThreadMessage,
historySnapshot: ThreadMessage[]
) => {
// Safety-net timeout: force-clear loading states if everything hangs
const OVERALL_TIMEOUT_MS = 240_000;
const safetyTimeout = setTimeout(() => {
@@ -387,7 +445,7 @@ const Conversations = () => {
const { invoke } = await import('@tauri-apps/api/core');
const recalledContext = await invoke<string | null>('recall_memory', {
skillId: 'conversations',
integrationId: selectedThreadId,
integrationId: sendingThreadId,
maxChunks: 10,
});
if (recalledContext) {
@@ -519,7 +577,10 @@ const Conversations = () => {
skillManager.callTool(skillId, toolName, toolArgs),
new Promise<never>((_, reject) =>
setTimeout(
() => reject(new Error(`Tool "${toolName}" timed out after ${TOOL_TIMEOUT_MS / 1000}s`)),
() =>
reject(
new Error(`Tool "${toolName}" timed out after ${TOOL_TIMEOUT_MS / 1000}s`)
),
TOOL_TIMEOUT_MS
)
),
@@ -575,17 +636,19 @@ const Conversations = () => {
} catch (err) {
// Remove the user message from persistent storage on error
// We'll use a thunk-like approach to access current state
dispatch((dispatch, getState) => {
dispatch((innerDispatch, getState) => {
const state = getState() as {
thread: { messagesByThreadId: Record<string, ThreadMessage[]> };
};
const persistedMessages = state.thread.messagesByThreadId[sendingThreadId] || [];
const currentMessages = persistedMessages.filter(m => m.id !== userMessage.id);
dispatch(updateMessagesForThread({ threadId: sendingThreadId, messages: currentMessages }));
innerDispatch(
updateMessagesForThread({ threadId: sendingThreadId, messages: currentMessages })
);
// Also remove from current view if this is the selected thread
if (sendingThreadId === selectedThreadId) {
dispatch(setSelectedThread(sendingThreadId));
innerDispatch(setSelectedThread(sendingThreadId));
}
});
@@ -602,6 +665,96 @@ const Conversations = () => {
}
};
const handleSendMessage = async (text?: string) => {
const trimmed = text ?? inputValue.trim();
if (!trimmed || !selectedThreadId || isSending) return;
// Check if another thread is already sending
if (activeThreadId && activeThreadId !== selectedThreadId) {
return; // Block sending from non-active threads
}
// Store the original thread ID to ensure response goes to correct thread
const sendingThreadId = selectedThreadId;
// Create stable user message and persist immediately
const userMessage: ThreadMessage = {
id: `msg_${Date.now()}_${Math.random()}`,
content: trimmed,
type: 'text',
extraMetadata: {},
sender: 'user',
createdAt: new Date().toISOString(),
};
// Immediately persist user message to both current view and persistent storage
dispatch(addMessageLocal({ threadId: sendingThreadId, message: userMessage }));
// Update current view if this is the selected thread
if (sendingThreadId === selectedThreadId) {
// Message is already added to persistent storage, reload current view
dispatch(setSelectedThread(sendingThreadId));
}
// Snapshot history for AI request (excluding the just-added user message since we'll add it manually)
const historySnapshot = messages.filter(
m => !m.id.startsWith('optimistic-') && m.id !== userMessage.id
);
setInputValue('');
setSendError(null);
setIsSending(true);
// Set this thread as active
dispatch(setActiveThread(sendingThreadId));
if (rustChat) {
// ── Rust path ────────────────────────────────────────────────────────
try {
const chatMessages = historySnapshot.map(m => ({
role: m.sender === 'user' ? 'user' : 'assistant',
content: m.content,
}));
const notionCtx = buildNotionContext(
notionProfile,
notionPages,
notionSummaries,
notionWorkspaceName
);
const authToken = (store.getState() as { auth: { token: string | null } }).auth.token;
if (!authToken) {
setSendError('Not authenticated');
setIsSending(false);
dispatch(setActiveThread(null));
return;
}
await chatSend({
threadId: sendingThreadId,
message: trimmed,
model: selectedModel,
authToken,
backendUrl: BACKEND_URL,
messages: chatMessages,
notionContext: notionCtx,
});
// setIsSending(false) and setActiveThread(null) happen in the onDone/onError event handlers
} catch (err) {
// invoke() itself failed (the chat loop reports errors via events)
const msg = err instanceof Error ? err.message : String(err);
setSendError(msg);
setIsSending(false);
dispatch(setActiveThread(null));
}
} else {
// ── Web fallback (existing orchestration logic) ───────────────────────
await handleSendMessageWeb(sendingThreadId, trimmed, userMessage, historySnapshot);
}
};
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
@@ -1010,6 +1163,24 @@ const Conversations = () => {
</div>
</div>
)}
{/* Tool call indicator — shown when Rust backend is executing a tool */}
{activeToolCall && isSending && (
<div className="flex items-center gap-2 px-1 py-1 text-xs text-stone-400">
<span className="animate-pulse">Running tool: {activeToolCall.name}</span>
</div>
)}
{/* Cancel button — shown when Rust backend is processing */}
{isSending && rustChat && (
<div className="flex justify-start px-1">
<button
onClick={() => {
if (selectedThreadId) void chatCancel(selectedThreadId);
}}
className="text-xs text-stone-400 hover:text-stone-200 transition-colors">
Cancel
</button>
</div>
)}
<div ref={messagesEndRef} />
</div>
) : (
+30 -60
View File
@@ -1,69 +1,39 @@
import { useEffect, useState } from 'react';
import { useNavigate, useSearchParams } from 'react-router-dom';
import DownloadScreen from '../components/DownloadScreen';
import OAuthLoginSection from '../components/oauth/OAuthLoginSection';
import TypewriterGreeting from '../components/TypewriterGreeting';
import { consumeLoginToken } from '../services/api/authApi';
import { setToken } from '../store/authSlice';
import { useAppDispatch } from '../store/hooks';
import { syncMemoryClientToken } from '../utils/tauriCommands';
interface LoginProps {
isWeb?: boolean;
}
const Login = () => {
const navigate = useNavigate();
const [searchParams] = useSearchParams();
const dispatch = useAppDispatch();
const [consumeError, setConsumeError] = useState<string | null>(null);
// Handle login token from URL (e.g. from Telegram bot or OAuth provider callback)
// Consume the token with the backend and store the returned JWT
useEffect(() => {
const loginToken = searchParams.get('token');
if (!loginToken) return;
let cancelled = false;
(async () => {
setConsumeError(null);
try {
const jwtToken = await consumeLoginToken(loginToken);
if (cancelled) return;
dispatch(setToken(jwtToken));
console.info('[memory] Login: dispatching syncMemoryClientToken after setToken');
await syncMemoryClientToken(jwtToken);
navigate('/onboarding/', { replace: true });
} catch (err) {
if (!cancelled) {
setConsumeError(err instanceof Error ? err.message : 'Login failed');
}
}
})();
return () => {
cancelled = true;
};
}, [searchParams, dispatch, navigate]);
if (consumeError) {
return (
<div className="min-h-full relative flex items-center justify-center">
<div className="relative z-10 max-w-md w-full mx-4 text-center">
<div className="glass rounded-3xl p-8 shadow-large animate-fade-up">
<p className="opacity-90 text-coral mb-4">{consumeError}</p>
<p className="text-sm opacity-70">
Please try logging in again with your preferred method.
</p>
</div>
</div>
</div>
);
}
const Login = ({ isWeb }: LoginProps) => {
const greetings = ['Hello HAL9000! 👋', "Let's cook! 🔥", 'The A-Team is here! 👊'];
return (
<div className="min-h-full relative flex items-center justify-center">
<div className="relative z-10 max-w-md w-full mx-4 text-center">
<div className="glass rounded-3xl p-8 shadow-large animate-fade-up">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-white mx-auto mb-4"></div>
<p className="opacity-70">Completing login...</p>
{/* Main content */}
<div className="relative z-10 max-w-md w-full mx-4 space-y-6">
{/* Welcome card */}
<div className="glass rounded-3xl p-8 text-center animate-fade-up shadow-large">
{/* Greeting */}
<TypewriterGreeting greetings={greetings} />
<p className="opacity-70 mb-8 leading-relaxed">
Welcome to AlphaHuman. Your Telegram assistant here to get you 10x more done in your
journey.
</p>
<p className="opacity-70 leading-relaxed">Are you ready for this?</p>
{/* Show OAuth login options in Tauri app, download screen on web */}
{!isWeb && (
<div className="mt-6">
<OAuthLoginSection />
</div>
)}
</div>
{isWeb && <DownloadScreen />}
</div>
</div>
);
+8 -9
View File
@@ -14,9 +14,9 @@ import SkillSetupModal from '../components/skills/SkillSetupModal';
import { deriveConnectionStatus, useSkillConnectionStatus } from '../lib/skills/hooks';
import { skillManager } from '../lib/skills/manager';
import type { SkillConnectionStatus, SkillHostConnectionState } from '../lib/skills/types';
import { deriveSkillSyncUiState } from './skillsSyncUi';
import { useAppSelector } from '../store/hooks';
import { IS_DEV } from '../utils/config';
import { deriveSkillSyncUiState } from './skillsSyncUi';
/** Format large numbers: 1200 → "1.2K", 1200000 → "1.2M" */
function formatNumber(n: number): string {
@@ -53,7 +53,10 @@ function SkillCard({ skill, onSetup }: SkillCardProps) {
| (SkillHostConnectionState & Record<string, unknown>)
| undefined;
const [manualSyncing, setManualSyncing] = useState(false);
const syncUi = useMemo(() => deriveSkillSyncUiState(skill.id, skillState), [skill.id, skillState]);
const syncUi = useMemo(
() => deriveSkillSyncUiState(skill.id, skillState),
[skill.id, skillState]
);
const isSyncing = manualSyncing || syncUi.isSyncing;
const handleSync = async (e: React.MouseEvent) => {
@@ -212,10 +215,11 @@ export default function Skills() {
}
const manifests = await invoke<Array<Record<string, unknown>>>('runtime_discover_skills');
const ALLOWED_SKILLS = new Set(['gmail', 'notion']);
const validManifests = manifests.filter(m => {
const id = m.id as string;
if (id.includes('_')) return false;
return true;
return ALLOWED_SKILLS.has(id);
});
const processed: SkillListEntry[] = validManifests
@@ -300,11 +304,7 @@ export default function Skills() {
) : (
<div className="space-y-2">
{sortedSkillsList.map(skill => (
<SkillCard
key={skill.id}
skill={skill}
onSetup={() => openSkillSetup(skill)}
/>
<SkillCard key={skill.id} skill={skill} onSetup={() => openSkillSetup(skill)} />
))}
</div>
)}
@@ -326,7 +326,6 @@ export default function Skills() {
}}
/>
)}
</div>
);
}
+130
View File
@@ -0,0 +1,130 @@
/**
* Chat Service — sends messages via Rust backend (Tauri) or falls back to
* frontend-driven orchestration (web mode).
*/
import { isTauri as coreIsTauri, invoke } from '@tauri-apps/api/core';
import { listen, type UnlistenFn } from '@tauri-apps/api/event';
// ─── Event payload types (must match Rust structs exactly — snake_case) ───────
export interface ChatToolCallEvent {
thread_id: string;
tool_name: string;
skill_id: string;
args: Record<string, unknown>;
round: number;
}
export interface ChatToolResultEvent {
thread_id: string;
tool_name: string;
skill_id: string;
output: string;
success: boolean;
round: number;
}
export interface ChatDoneEvent {
thread_id: string;
full_response: string;
rounds_used: number;
total_input_tokens: number;
total_output_tokens: number;
}
export interface ChatErrorEvent {
thread_id: string;
message: string;
error_type: 'network' | 'timeout' | 'tool_error' | 'inference' | 'cancelled';
round: number | null;
}
// ─── Listener setup ───────────────────────────────────────────────────────────
export interface ChatEventListeners {
onToolCall?: (event: ChatToolCallEvent) => void;
onToolResult?: (event: ChatToolResultEvent) => void;
onDone?: (event: ChatDoneEvent) => void;
onError?: (event: ChatErrorEvent) => void;
}
/**
* Subscribe to chat events from the Rust backend.
* Returns a cleanup function that removes all listeners.
* Only works in Tauri mode.
*/
export async function subscribeChatEvents(listeners: ChatEventListeners): Promise<() => void> {
const unlisteners: UnlistenFn[] = [];
if (listeners.onToolCall) {
const cb = listeners.onToolCall;
unlisteners.push(await listen<ChatToolCallEvent>('chat:tool_call', e => cb(e.payload)));
}
if (listeners.onToolResult) {
const cb = listeners.onToolResult;
unlisteners.push(await listen<ChatToolResultEvent>('chat:tool_result', e => cb(e.payload)));
}
if (listeners.onDone) {
const cb = listeners.onDone;
unlisteners.push(await listen<ChatDoneEvent>('chat:done', e => cb(e.payload)));
}
if (listeners.onError) {
const cb = listeners.onError;
unlisteners.push(await listen<ChatErrorEvent>('chat:error', e => cb(e.payload)));
}
return () => {
for (const unlisten of unlisteners) {
unlisten();
}
};
}
// ─── Send message ─────────────────────────────────────────────────────────────
export interface ChatSendParams {
threadId: string;
message: string;
model: string;
authToken: string;
backendUrl: string;
messages: Array<{
role: string;
content: string;
tool_calls?: unknown[];
tool_call_id?: string;
}>;
notionContext?: string | null;
}
/**
* Send a message via the Rust chat_send command.
* Returns immediately — results arrive via events.
* Tauri v2 converts camelCase param names to snake_case for the Rust command.
*/
export async function chatSend(params: ChatSendParams): Promise<void> {
await invoke('chat_send', {
threadId: params.threadId,
message: params.message,
model: params.model,
authToken: params.authToken,
backendUrl: params.backendUrl,
messages: params.messages,
notionContext: params.notionContext ?? null,
});
}
/**
* Cancel an in-flight chat request.
*/
export async function chatCancel(threadId: string): Promise<boolean> {
return await invoke<boolean>('chat_cancel', { threadId });
}
/**
* Check if we should use the Rust backend for chat.
* Returns true when running in Tauri on desktop.
*/
export function useRustChat(): boolean {
return coreIsTauri();
}
+56 -5
View File
@@ -319,6 +319,11 @@
resolved "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz"
integrity sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==
"@dimforge/rapier3d-compat@~0.12.0":
version "0.12.0"
resolved "https://registry.yarnpkg.com/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz#7b3365e1dfdc5cd957b45afe920b4ac06c7cd389"
integrity sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==
"@esbuild/aix-ppc64@0.27.2":
version "0.27.2"
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz#521cbd968dcf362094034947f76fa1b18d2d403c"
@@ -1294,7 +1299,7 @@
dependencies:
postcss-selector-parser "6.0.10"
"@tauri-apps/api@2.10.1":
"@tauri-apps/api@^2.10.0":
version "2.10.1"
resolved "https://registry.yarnpkg.com/@tauri-apps/api/-/api-2.10.1.tgz#57c1bae6114ec33d977eb2b50dfefc25fa84fc93"
integrity sha512-hKL/jWf293UDSUN09rR69hrToyIXBb8CjGaWC7gfinvnQrBVvnLr08FeFi38gxtugAVyVcTa5/FD/Xnkb1siBw==
@@ -1359,9 +1364,9 @@
resolved "https://registry.yarnpkg.com/@tauri-apps/cli-win32-x64-msvc/-/cli-win32-x64-msvc-2.9.6.tgz#d58c9f8af835b7e4fc30e201e979342c70bea426"
integrity sha512-ldWuWSSkWbKOPjQMJoYVj9wLHcOniv7diyI5UAJ4XsBdtaFB0pKHQsqw/ItUma0VXGC7vB4E9fZjivmxur60aw==
"@tauri-apps/cli@^2":
"@tauri-apps/cli@2.9.6":
version "2.9.6"
resolved "https://registry.npmjs.org/@tauri-apps/cli/-/cli-2.9.6.tgz"
resolved "https://registry.yarnpkg.com/@tauri-apps/cli/-/cli-2.9.6.tgz#f15ae8e03bf48308055c15ab25b439bed9906bc9"
integrity sha512-3xDdXL5omQ3sPfBfdC8fCtDKcnyV7OqyzQgfyT5P3+zY6lcPqIYKQBvUasNvppi21RSdfhy44ttvJmftb0PCDw==
optionalDependencies:
"@tauri-apps/cli-darwin-arm64" "2.9.6"
@@ -1461,6 +1466,11 @@
minimatch "^9.0.0"
parse-imports-exports "^0.2.4"
"@tweenjs/tween.js@~23.1.3":
version "23.1.3"
resolved "https://registry.yarnpkg.com/@tweenjs/tween.js/-/tween.js-23.1.3.tgz#eff0245735c04a928bb19c026b58c2a56460539d"
integrity sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==
"@types/aria-query@^5.0.1":
version "5.0.4"
resolved "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz"
@@ -1661,11 +1671,29 @@
resolved "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz"
integrity sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==
"@types/stats.js@*":
version "0.17.4"
resolved "https://registry.yarnpkg.com/@types/stats.js/-/stats.js-0.17.4.tgz#1933e5ff153a23c7664487833198d685c22e791e"
integrity sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==
"@types/statuses@^2.0.6":
version "2.0.6"
resolved "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.6.tgz"
integrity sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA==
"@types/three@^0.183.1":
version "0.183.1"
resolved "https://registry.yarnpkg.com/@types/three/-/three-0.183.1.tgz#d812d028b38ad68843725e3e7bd3268607cef150"
integrity sha512-f2Pu5Hrepfgavttdye3PsH5RWyY/AvdZQwIVhrc4uNtvF7nOWJacQKcoVJn0S4f0yYbmAE6AR+ve7xDcuYtMGw==
dependencies:
"@dimforge/rapier3d-compat" "~0.12.0"
"@tweenjs/tween.js" "~23.1.3"
"@types/stats.js" "*"
"@types/webxr" ">=0.5.17"
"@webgpu/types" "*"
fflate "~0.8.2"
meshoptimizer "~1.0.1"
"@types/unist@*", "@types/unist@^3.0.0":
version "3.0.3"
resolved "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz"
@@ -1681,6 +1709,11 @@
resolved "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz"
integrity sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==
"@types/webxr@>=0.5.17":
version "0.5.24"
resolved "https://registry.yarnpkg.com/@types/webxr/-/webxr-0.5.24.tgz#734d5d90dadc5809a53e422726c60337fa2f4a44"
integrity sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==
"@types/which@^2.0.1":
version "2.0.2"
resolved "https://registry.npmjs.org/@types/which/-/which-2.0.2.tgz"
@@ -2109,6 +2142,11 @@
dependencies:
"@wdio/logger" "9.18.0"
"@webgpu/types@*":
version "0.1.69"
resolved "https://registry.yarnpkg.com/@webgpu/types/-/types-0.1.69.tgz#6b849bf370a1f29c78bd3aeba8e84c1150b237f2"
integrity sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==
"@xmldom/xmldom@^0.9.5":
version "0.9.8"
resolved "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.8.tgz"
@@ -4127,6 +4165,11 @@ fdir@^6.5.0:
resolved "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz"
integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
fflate@~0.8.2:
version "0.8.2"
resolved "https://registry.yarnpkg.com/fflate/-/fflate-0.8.2.tgz#fc8631f5347812ad6028bbe4a2308b2792aa1dea"
integrity sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==
figures@^6.1.0:
version "6.1.0"
resolved "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz"
@@ -5594,6 +5637,11 @@ merge2@^1.3.0:
resolved "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz"
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
meshoptimizer@~1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/meshoptimizer/-/meshoptimizer-1.0.1.tgz#c3ef0d509a8b84ac562493dba5a108fd67fa76dc"
integrity sha512-Vix+QlA1YYT3FwmBBZ+49cE5y/b+pRrcXKqGpS5ouh33d3lSp2PoTpCw19E0cKDFWalembrHnIaZetf27a+W2g==
micromark-core-commonmark@^2.0.0:
version "2.0.3"
resolved "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz"
@@ -7500,7 +7548,6 @@ stringify-entities@^4.0.0:
character-entities-legacy "^3.0.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
name strip-ansi-cjs
version "6.0.1"
resolved "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -7689,6 +7736,11 @@ thenify-all@^1.0.0:
dependencies:
any-promise "^1.0.0"
three@^0.183.2:
version "0.183.2"
resolved "https://registry.yarnpkg.com/three/-/three-0.183.2.tgz#606e3195bf210ef8d1eaaca2ab8c59d92d2bbc18"
integrity sha512-di3BsL2FEQ1PA7Hcvn4fyJOlxRRgFYBpMTcyOgkwJIaDOdJMebEFPA+t98EvjuljDx4hNulAGwF6KIjtwI5jgQ==
timers-browserify@^2.0.4:
version "2.0.12"
resolved "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz"
@@ -8346,7 +8398,6 @@ workerpool@^6.5.1:
integrity sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
name wrap-ansi-cjs
version "7.0.0"
resolved "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==