@clawlabz/clawskin 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +98 -0
- package/CONTRIBUTING.md +40 -0
- package/LICENSE +21 -0
- package/README.md +137 -0
- package/docs/ARCHITECTURE.md +132 -0
- package/docs/PROGRESS.md +15 -0
- package/docs/ROADMAP.md +130 -0
- package/package.json +27 -0
- package/public/_headers +13 -0
- package/public/_redirects +1 -0
- package/public/app.html +595 -0
- package/public/css/app.css +379 -0
- package/public/css/style.css +415 -0
- package/public/index.html +324 -0
- package/public/js/app/AgentSlot.js +596 -0
- package/public/js/app/AgentStateMapper.js +213 -0
- package/public/js/app/ClawSkinApp.js +551 -0
- package/public/js/app/ConnectionPanel.js +142 -0
- package/public/js/app/DeviceIdentity.js +118 -0
- package/public/js/app/GatewayClient.js +284 -0
- package/public/js/app/SettingsManager.js +47 -0
- package/public/js/character/AnimationManager.js +69 -0
- package/public/js/character/BubbleManager.js +157 -0
- package/public/js/character/CharacterSprite.js +98 -0
- package/public/js/game.js +116 -0
- package/public/js/pets/Pet.js +303 -0
- package/public/js/pets/PetManager.js +102 -0
- package/public/js/scenes/CafeScene.js +594 -0
- package/public/js/scenes/HackerScene.js +527 -0
- package/public/js/scenes/OfficeScene.js +404 -0
- package/public/js/sprites/SpriteGenerator.js +927 -0
- package/public/js/state/AgentStateSync.js +89 -0
- package/public/js/state/DemoMode.js +65 -0
- package/public/js/ui/CharacterEditor.js +145 -0
- package/public/js/ui/ScenePicker.js +29 -0
- package/serve.cjs +132 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AgentStateMapper.js — Maps OpenClaw Gateway events to ClawSkin character states
|
|
3
|
+
* Listens to real-time WS events and drives the pixel character animations
|
|
4
|
+
*/
|
|
5
|
+
class AgentStateMapper {
|
|
6
|
+
constructor(character, options = {}) {
|
|
7
|
+
this.character = character;
|
|
8
|
+
this.currentState = 'idle';
|
|
9
|
+
this.lastActivityTime = Date.now();
|
|
10
|
+
this.idleTimeout = options.idleTimeout || 30 * 60 * 1000; // 30min → sleeping
|
|
11
|
+
this.sleepCheckInterval = null;
|
|
12
|
+
this.activeRunId = null;
|
|
13
|
+
this.agentId = options.agentId || 'main';
|
|
14
|
+
this.sessionKeys = new Set(options.sessionKeys || ['main']);
|
|
15
|
+
this.onStateChange = options.onStateChange || null;
|
|
16
|
+
|
|
17
|
+
// Tool name → state mapping
|
|
18
|
+
this.toolStateMap = {
|
|
19
|
+
'exec': 'executing',
|
|
20
|
+
'write': 'typing',
|
|
21
|
+
'edit': 'typing',
|
|
22
|
+
'read': 'browsing',
|
|
23
|
+
'web_fetch': 'browsing',
|
|
24
|
+
'web_search': 'browsing',
|
|
25
|
+
'browser': 'browsing',
|
|
26
|
+
'image': 'browsing',
|
|
27
|
+
'message': 'typing',
|
|
28
|
+
'tts': 'typing',
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
this._startIdleCheck();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Process a Gateway event and update character state
|
|
36
|
+
*/
|
|
37
|
+
handleEvent(event) {
|
|
38
|
+
if (!event || !event.event) return;
|
|
39
|
+
|
|
40
|
+
switch (event.event) {
|
|
41
|
+
case 'chat':
|
|
42
|
+
this._handleChatEvent(event.payload);
|
|
43
|
+
break;
|
|
44
|
+
case 'agent':
|
|
45
|
+
this._handleAgentEvent(event.payload);
|
|
46
|
+
break;
|
|
47
|
+
case 'presence':
|
|
48
|
+
// Could show connected instances
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
_handleChatEvent(payload) {
|
|
54
|
+
if (!payload) return;
|
|
55
|
+
|
|
56
|
+
// Accept events from any session belonging to this agent
|
|
57
|
+
// (Events are pre-routed by agentId in ClawSkinApp, so this is a safety check)
|
|
58
|
+
|
|
59
|
+
switch (payload.state) {
|
|
60
|
+
case 'delta':
|
|
61
|
+
// Agent is streaming a response
|
|
62
|
+
this._setCharState('typing', this._extractStreamText(payload));
|
|
63
|
+
this.activeRunId = payload.runId || null;
|
|
64
|
+
break;
|
|
65
|
+
|
|
66
|
+
case 'final':
|
|
67
|
+
// Agent finished responding
|
|
68
|
+
this.activeRunId = null;
|
|
69
|
+
this._setCharState('idle');
|
|
70
|
+
// Show a snippet of the response as speech bubble
|
|
71
|
+
const text = this._extractFinalText(payload);
|
|
72
|
+
if (text) {
|
|
73
|
+
this.character.showBubble(
|
|
74
|
+
text.length > 60 ? text.slice(0, 57) + '...' : text,
|
|
75
|
+
'speech', 4000
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
break;
|
|
79
|
+
|
|
80
|
+
case 'aborted':
|
|
81
|
+
this.activeRunId = null;
|
|
82
|
+
this._setCharState('idle');
|
|
83
|
+
break;
|
|
84
|
+
|
|
85
|
+
case 'error':
|
|
86
|
+
this.activeRunId = null;
|
|
87
|
+
this._setCharState('error');
|
|
88
|
+
this.character.showBubble('Something went wrong...', 'speech', 3000);
|
|
89
|
+
// Return to idle after 3s
|
|
90
|
+
setTimeout(() => {
|
|
91
|
+
if (this.currentState === 'error') this._setCharState('idle');
|
|
92
|
+
}, 3000);
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
_handleAgentEvent(payload) {
|
|
98
|
+
if (!payload) return;
|
|
99
|
+
|
|
100
|
+
// Tool stream events
|
|
101
|
+
if (payload.stream === 'tool') {
|
|
102
|
+
const data = payload.data || {};
|
|
103
|
+
const toolName = data.name || '';
|
|
104
|
+
const phase = data.phase || '';
|
|
105
|
+
|
|
106
|
+
if (phase === 'start') {
|
|
107
|
+
const mappedState = this.toolStateMap[toolName] || 'executing';
|
|
108
|
+
const bubble = this._getToolBubble(toolName, data.args);
|
|
109
|
+
this._setCharState(mappedState, bubble);
|
|
110
|
+
} else if (phase === 'result') {
|
|
111
|
+
// Tool finished, back to thinking (model will process result)
|
|
112
|
+
this._setCharState('thinking');
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Lifecycle events (model fallback, etc.)
|
|
118
|
+
if (payload.stream === 'lifecycle' || payload.stream === 'fallback') {
|
|
119
|
+
this._setCharState('thinking');
|
|
120
|
+
this.character.showBubble('Switching approach...', 'thought', 2000);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Compaction events
|
|
125
|
+
if (payload.stream === 'compaction') {
|
|
126
|
+
const phase = payload.data?.phase;
|
|
127
|
+
if (phase === 'start') {
|
|
128
|
+
this._setCharState('thinking');
|
|
129
|
+
this.character.showBubble('Organizing thoughts...', 'thought', 3000);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
_setCharState(state, bubbleText) {
|
|
135
|
+
if (state === this.currentState && !bubbleText) return;
|
|
136
|
+
this.currentState = state;
|
|
137
|
+
this.lastActivityTime = Date.now();
|
|
138
|
+
this.character.setState(state);
|
|
139
|
+
|
|
140
|
+
if (bubbleText) {
|
|
141
|
+
const type = state === 'thinking' ? 'thought' : 'speech';
|
|
142
|
+
this.character.showBubble(bubbleText, type, 4000);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (this.onStateChange) this.onStateChange(state);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
_extractStreamText(payload) {
|
|
149
|
+
// During streaming, show a thinking bubble
|
|
150
|
+
if (!this.character.bubbleManager?.activeBubble) {
|
|
151
|
+
return 'Writing...';
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
_extractFinalText(payload) {
|
|
157
|
+
if (!payload?.message) return null;
|
|
158
|
+
const msg = payload.message;
|
|
159
|
+
if (typeof msg.text === 'string') return msg.text;
|
|
160
|
+
if (Array.isArray(msg.content)) {
|
|
161
|
+
for (const block of msg.content) {
|
|
162
|
+
if (block.type === 'text' && block.text) {
|
|
163
|
+
return block.text;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
_getToolBubble(toolName, args) {
|
|
171
|
+
switch (toolName) {
|
|
172
|
+
case 'exec': return args?.command ? `$ ${args.command.slice(0, 40)}` : 'Running command...';
|
|
173
|
+
case 'write': return args?.path ? `Writing ${this._basename(args.path)}` : 'Writing file...';
|
|
174
|
+
case 'edit': return args?.path ? `Editing ${this._basename(args.path)}` : 'Editing...';
|
|
175
|
+
case 'read': return args?.path ? `Reading ${this._basename(args.path)}` : 'Reading file...';
|
|
176
|
+
case 'web_fetch': return 'Fetching page...';
|
|
177
|
+
case 'web_search': return 'Searching web...';
|
|
178
|
+
case 'browser': return 'Using browser...';
|
|
179
|
+
case 'image': return 'Analyzing image...';
|
|
180
|
+
case 'message': return 'Sending message...';
|
|
181
|
+
default: return `Using ${toolName}...`;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
_basename(path) {
|
|
186
|
+
return (path || '').split('/').pop() || path;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
_startIdleCheck() {
|
|
190
|
+
this.sleepCheckInterval = setInterval(() => {
|
|
191
|
+
if (this.currentState === 'idle' || this.currentState === 'sleeping') {
|
|
192
|
+
const elapsed = Date.now() - this.lastActivityTime;
|
|
193
|
+
if (elapsed > this.idleTimeout && this.currentState !== 'sleeping') {
|
|
194
|
+
this._setCharState('sleeping');
|
|
195
|
+
this.character.showBubble('zzZ...', 'thought', 5000);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}, 60000);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
destroy() {
|
|
202
|
+
if (this.sleepCheckInterval) {
|
|
203
|
+
clearInterval(this.sleepCheckInterval);
|
|
204
|
+
this.sleepCheckInterval = null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
addSessionKey(key) {
|
|
209
|
+
this.sessionKeys.add(key);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (typeof window !== 'undefined') window.AgentStateMapper = AgentStateMapper;
|