Feat/openclaw work - #131 (#151)

* feat: add initial project structure and documentation

- Introduced the GNU General Public License (GPL) v3 in LICENSE file.
- Added MCP configuration in .claude/mcp.json for server integration.
- Created architecture documentation in docs/ARCHITECTURE.md outlining the platform's design and components.
- Defined MVP specifications in docs/MVP.md for the Telegram-based Agent Assistant.
- Established API reference for team management in docs/teams-api-reference.md.
- Set up basic HTML structure in public/index.html and added logo image in public/logo.png.

* feat: add initial project documentation and HTML structure

- Introduced CODE_OF_CONDUCT.md to establish community guidelines and standards for behavior.
- Created CONTRIBUTING.md to outline contribution process, development setup, and project conventions.
- Added SECURITY.md to define the security policy, supported versions, and reporting procedures for vulnerabilities.
- Established basic HTML structure in index.html for the application interface.

* chore: remove hello-python skill files

- Deleted skill.json and skill.py files for the Hello Python example runtime skill, as they are no longer needed in the project.

* feat: port tinyhuman agent runtime from ZeroClaw into Tauri backend

Port daemon supervisor, health registry, security (policy, secrets, audit,
pairing), agent traits, and config modules from ZeroClaw (MIT) into a new
tinyhuman/ module under src-tauri/src/. The daemon auto-starts on desktop
and shuts down gracefully on app exit via CancellationToken.

- health: global HealthRegistry with component tracking and JSON snapshots
- security/policy: SecurityPolicy with command validation, risk levels, rate limiting
- security/secrets: ChaCha20-Poly1305 SecretStore with legacy XOR migration
- security/audit: AuditLogger with JSON-line events and log rotation
- security/pairing: PairingGuard with brute-force protection and SHA-256 hashing
- security/traits: Sandbox trait + NoopSandbox
- config: minimal DaemonConfig with autonomy, reliability, secrets, audit sub-configs
- daemon: supervisor with health state writer emitting Tauri events
- agent/traits: Provider, Tool, Memory, Observer, RuntimeAdapter traits + Noop impls
- commands/tinyhuman: Tauri commands for health, security policy, encrypt/decrypt
- 185 inline unit tests across all modules
- README updated with custom inference/tunneling/memory positioning

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat: update README to reflect AlphaHuman Mk1 branding and enhanced description

- Changed project title to "AlphaHuman Mk1" for clarity.
- Revised project description to emphasize user-friendly AI capabilities and the use of the Neocortex Mk1 model.
- Removed outdated sections on custom inference, tunneling, and memory, streamlining the content for better readability.

* update readme

* Port zeroclaw runtime into tinyhuman

* Replace CLI mentions with UI language

* Split gateway module into smaller units

* Split channels and config schema modules

* Fix tinyhuman build, tests, and tunnel integration

* feat(tinyhuman): add missing modules and ui-friendly services

* refactor: rename tinyhuman to alphahuman

* chore: remove bottom text from Welcome component

* feat(settings): add tauri command console

* feat(daemon): enhance daemon mode handling and integrate rustls with ring feature

* feat(settings): implement comprehensive configuration management in TauriCommandsPanel

* refactor(TauriCommandsPanel): streamline error handling and enhance async function usage

* feat(settings): add skill management functionality to TauriCommandsPanel

* style(TauriCommandsPanel): update input styles for improved readability and user experience

* feat(settings): add Skills and Agent Chat panels with navigation and integration management

* feat(settings): implement browser access management in SkillsPanel and enhance AgentChatPanel with local storage functionality

* Implement inference API integration in Conversations component

- Added inference API service to handle chat completions and model management.
- Updated Conversations component to fetch available models on mount and allow model selection.
- Enhanced message sending functionality to utilize the inference API for generating responses.
- Introduced state management for loading models and handling errors during API calls.
- Refactored optimistic message handling and send status management for improved user experience.

---------

Co-authored-by: Steven Enamakel <enamakel@vezures.xyz>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Steven Enamakel <31011319+senamakel@users.noreply.github.com>
This commit is contained in:
Mega Mind
2026-02-26 19:58:21 +04:00
committed by GitHub
co-authored by Steven Enamakel Claude Opus 4.6 Steven Enamakel
parent ca11b001c8
commit 694dcfe3cc
6 changed files with 1150 additions and 672 deletions
File diff suppressed because it is too large Load Diff
+96 -14
View File
@@ -9,21 +9,22 @@ import {
import Markdown from 'react-markdown';
import { useNavigate, useParams } from 'react-router-dom';
import { inferenceApi, type ModelInfo } from '../services/api/inferenceApi';
import { useAppDispatch, useAppSelector } from '../store/hooks';
import {
addInferenceResponse,
addOptimisticMessage,
clearCreateStatus,
clearDeleteStatus,
clearPurgeStatus,
clearSelectedThread,
clearSendError,
createThread,
deleteThread,
fetchSuggestedQuestions,
fetchThreadMessages,
fetchThreads,
purgeThreads,
sendMessage,
removeOptimisticMessages,
setLastViewed,
setPanelWidth,
setSelectedThread,
@@ -60,8 +61,6 @@ const Conversations = () => {
createStatus,
deleteStatus,
purgeStatus,
sendStatus,
sendError,
panelWidth,
lastViewedAt,
suggestedQuestions,
@@ -73,6 +72,13 @@ const Conversations = () => {
const [inputValue, setInputValue] = useState('');
const [searchQuery, setSearchQuery] = useState('');
const [copiedMessageId, setCopiedMessageId] = useState<string | null>(null);
// Inference model state
const [availableModels, setAvailableModels] = useState<ModelInfo[]>([]);
const [selectedModel, setSelectedModel] = useState('neocortex-mk1');
const [isLoadingModels, setIsLoadingModels] = useState(false);
const [isSending, setIsSending] = useState(false);
const [sendError, setSendError] = useState<string | null>(null);
const isDragging = useRef(false);
const messagesEndRef = useRef<HTMLDivElement>(null);
const lastPanelWidthRef = useRef(panelWidth);
@@ -134,6 +140,23 @@ const Conversations = () => {
[panelWidth, dispatch]
);
// Fetch available inference models on mount
useEffect(() => {
setIsLoadingModels(true);
inferenceApi
.listModels()
.then(data => {
if (data.data.length > 0) {
setAvailableModels(data.data);
setSelectedModel(data.data[0].id);
}
})
.catch(() => {
// Keep default model on failure
})
.finally(() => setIsLoadingModels(false));
}, []);
// Fetch threads on mount
useEffect(() => {
dispatch(fetchThreads());
@@ -196,9 +219,9 @@ const Conversations = () => {
// Clear send error when user starts typing again
useEffect(() => {
if (sendError && inputValue.length > 0) {
dispatch(clearSendError());
setSendError(null);
}
}, [inputValue, sendError, dispatch]);
}, [inputValue, sendError]);
const handleSelectThread = (threadId: string) => {
if (threadId === selectedThreadId) return;
@@ -228,12 +251,44 @@ const Conversations = () => {
}
};
const handleSendMessage = (text?: string) => {
const handleSendMessage = async (text?: string) => {
const trimmed = text ?? inputValue.trim();
if (!trimmed || !selectedThreadId || sendStatus === 'loading') return;
if (!trimmed || !selectedThreadId || isSending) return;
// Snapshot history before the optimistic update (exclude stale optimistic msgs)
const historySnapshot = messages.filter(m => !m.id.startsWith('optimistic-'));
dispatch(addOptimisticMessage({ content: trimmed }));
setInputValue('');
dispatch(sendMessage({ threadId: selectedThreadId, message: trimmed }));
setSendError(null);
setIsSending(true);
try {
const chatMessages = [
...historySnapshot.map(m => ({
role: (m.sender === 'user' ? 'user' : 'assistant') as 'user' | 'assistant',
content: m.content,
})),
{ role: 'user' as const, content: trimmed },
];
const response = await inferenceApi.createChatCompletion({
model: selectedModel,
messages: chatMessages,
});
const content = response.choices[0]?.message?.content ?? '';
dispatch(addInferenceResponse({ content }));
} catch (err) {
dispatch(removeOptimisticMessages());
const msg =
err && typeof err === 'object' && 'error' in err
? String((err as { error: unknown }).error)
: 'Failed to get response';
setSendError(msg);
} finally {
setIsSending(false);
}
};
const handleInputKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
@@ -683,7 +738,7 @@ const Conversations = () => {
</div>
))}
{/* Typing indicator (#14) */}
{sendStatus === 'loading' && (
{isSending && (
<div className="flex justify-start">
<div className="bg-white/5 rounded-2xl rounded-bl-md px-4 py-3">
<div className="flex items-center gap-1">
@@ -712,7 +767,7 @@ const Conversations = () => {
key={i}
type="button"
onClick={() => handleSendMessage(s.text)}
disabled={sendStatus === 'loading'}
disabled={isSending}
className="flex-shrink-0 px-3 py-1.5 rounded-lg text-[12px] whitespace-nowrap bg-white/5 text-stone-400 hover:bg-white/10 transition-colors disabled:opacity-50 disabled:cursor-not-allowed">
{s.text}
</button>
@@ -723,11 +778,38 @@ const Conversations = () => {
{/* Message Input */}
<div className="flex-shrink-0 border-t border-white/10 px-4 py-3">
{/* Model selector */}
<div className="flex items-center gap-2 mb-2">
{isLoadingModels ? (
<span className="text-xs text-stone-600">Loading models</span>
) : (
<>
<span className="text-xs text-stone-500">Model</span>
<select
value={selectedModel}
onChange={e => setSelectedModel(e.target.value)}
disabled={isSending}
className="bg-white/5 border border-white/10 rounded-lg px-2 py-1 text-xs text-stone-300 focus:outline-none focus:ring-1 focus:ring-primary-500/50 disabled:opacity-50 cursor-pointer">
{availableModels.length > 0 ? (
availableModels.map(m => (
<option key={m.id} value={m.id} className="bg-stone-900">
{m.id}
</option>
))
) : (
<option value={selectedModel} className="bg-stone-900">
{selectedModel}
</option>
)}
</select>
</>
)}
</div>
{sendError && (
<div className="flex items-center justify-between mb-2">
<p className="text-xs text-coral-500">{sendError}</p>
<button
onClick={() => dispatch(clearSendError())}
onClick={() => setSendError(null)}
className="text-xs text-stone-500 hover:text-stone-300 transition-colors ml-2 flex-shrink-0">
Dismiss
</button>
@@ -744,9 +826,9 @@ const Conversations = () => {
/>
<button
onClick={() => handleSendMessage()}
disabled={!inputValue.trim() || sendStatus === 'loading'}
disabled={!inputValue.trim() || isSending}
className="p-2.5 rounded-xl bg-primary-600 hover:bg-primary-500 disabled:opacity-40 disabled:cursor-not-allowed transition-colors flex-shrink-0">
{sendStatus === 'loading' ? (
{isSending ? (
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle
className="opacity-25"
+99
View File
@@ -0,0 +1,99 @@
import { apiClient } from '../apiClient';
// ── Request types ────────────────────────────────────────────────────────────
export type ChatRole = 'system' | 'user' | 'assistant';
export interface ChatMessage {
role: ChatRole;
content: string;
}
export interface ChatCompletionRequest {
model: string;
messages: ChatMessage[];
stream?: boolean;
temperature?: number;
max_tokens?: number;
}
export interface TextCompletionRequest {
model: string;
prompt: string;
stream?: boolean;
temperature?: number;
max_tokens?: number;
}
// ── Response types (OpenAI-compatible) ───────────────────────────────────────
export interface ModelInfo {
id: string;
object: string;
created: number;
owned_by: string;
}
export interface ModelsListResponse {
object: string;
data: ModelInfo[];
}
export interface ChatCompletionChoice {
index: number;
message: ChatMessage;
finish_reason: string | null;
}
export interface TextCompletionChoice {
index: number;
text: string;
finish_reason: string | null;
}
export interface CompletionUsage {
prompt_tokens: number;
completion_tokens: number;
total_tokens: number;
}
export interface ChatCompletionResponse {
id: string;
object: string;
created: number;
model: string;
choices: ChatCompletionChoice[];
usage: CompletionUsage;
}
export interface TextCompletionResponse {
id: string;
object: string;
created: number;
model: string;
choices: TextCompletionChoice[];
usage: CompletionUsage;
}
// ── API ───────────────────────────────────────────────────────────────────────
export const inferenceApi = {
/** GET /openai/v1/models — list available models */
listModels: async (): Promise<ModelsListResponse> => {
return apiClient.get<ModelsListResponse>('/openai/v1/models');
},
/** POST /openai/v1/chat/completions — create a chat completion */
createChatCompletion: async (
body: ChatCompletionRequest
): Promise<ChatCompletionResponse> => {
return apiClient.post<ChatCompletionResponse>('/openai/v1/chat/completions', body);
},
/** POST /openai/v1/completions — create a text completion */
createCompletion: async (
body: TextCompletionRequest
): Promise<TextCompletionResponse> => {
return apiClient.post<TextCompletionResponse>('/openai/v1/completions', body);
},
};
+70
View File
@@ -0,0 +1,70 @@
import type { ApiResponse } from '../../types/api';
import { apiClient } from '../apiClient';
// ── Types ─────────────────────────────────────────────────────────────────────
export interface Tunnel {
id: string;
name: string;
description?: string;
isActive: boolean;
createdAt: string;
updatedAt: string;
}
export interface TunnelBandwidthUsage {
usedBytes: number;
limitBytes: number;
cycleStartDate: string;
cycleEndDate: string;
}
export interface CreateTunnelRequest {
name: string;
description?: string;
}
export interface UpdateTunnelRequest {
name?: string;
description?: string;
isActive?: boolean;
}
// ── API ───────────────────────────────────────────────────────────────────────
export const tunnelsApi = {
/** POST /tunnels — create a new webhook tunnel */
createTunnel: async (body: CreateTunnelRequest): Promise<Tunnel> => {
const response = await apiClient.post<ApiResponse<Tunnel>>('/tunnels', body);
return response.data;
},
/** GET /tunnels — list user's webhook tunnels */
getTunnels: async (): Promise<Tunnel[]> => {
const response = await apiClient.get<ApiResponse<Tunnel[]>>('/tunnels');
return response.data;
},
/** GET /tunnels/bandwidth — get bandwidth usage for current billing cycle */
getBandwidthUsage: async (): Promise<TunnelBandwidthUsage> => {
const response = await apiClient.get<ApiResponse<TunnelBandwidthUsage>>('/tunnels/bandwidth');
return response.data;
},
/** GET /tunnels/:id — get a specific webhook tunnel */
getTunnel: async (id: string): Promise<Tunnel> => {
const response = await apiClient.get<ApiResponse<Tunnel>>(`/tunnels/${id}`);
return response.data;
},
/** PATCH /tunnels/:id — update a webhook tunnel */
updateTunnel: async (id: string, body: UpdateTunnelRequest): Promise<Tunnel> => {
const response = await apiClient.patch<ApiResponse<Tunnel>>(`/tunnels/${id}`, body);
return response.data;
},
/** DELETE /tunnels/:id — delete a webhook tunnel */
deleteTunnel: async (id: string): Promise<void> => {
await apiClient.delete<ApiResponse<unknown>>(`/tunnels/${id}`);
},
};
+11
View File
@@ -218,6 +218,16 @@ const threadSlice = createSlice({
createdAt: new Date().toISOString(),
});
},
addInferenceResponse: (state, action: { payload: { content: string } }) => {
state.messages.push({
id: `inference-${Date.now()}`,
content: action.payload.content,
type: 'text',
extraMetadata: {},
sender: 'agent',
createdAt: new Date().toISOString(),
});
},
removeOptimisticMessages: state => {
state.messages = state.messages.filter(m => !m.id.startsWith('optimistic-'));
},
@@ -345,6 +355,7 @@ export const {
clearDeleteStatus,
clearPurgeStatus,
addOptimisticMessage,
addInferenceResponse,
removeOptimisticMessages,
clearSendError,
clearSuggestedQuestions,
+567 -319
View File
File diff suppressed because it is too large Load Diff