@chrysb/alphaclaw 0.3.3 → 0.3.4-beta.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/bin/alphaclaw.js +18 -0
- package/lib/plugin/usage-tracker/index.js +308 -0
- package/lib/plugin/usage-tracker/openclaw.plugin.json +8 -0
- package/lib/public/css/explorer.css +51 -1
- package/lib/public/css/shell.css +3 -1
- package/lib/public/css/theme.css +35 -0
- package/lib/public/js/app.js +73 -24
- package/lib/public/js/components/file-tree.js +181 -6
- package/lib/public/js/components/file-viewer.js +43 -20
- package/lib/public/js/components/segmented-control.js +33 -0
- package/lib/public/js/components/sidebar.js +14 -32
- package/lib/public/js/components/telegram-workspace/index.js +353 -0
- package/lib/public/js/components/telegram-workspace/manage.js +397 -0
- package/lib/public/js/components/telegram-workspace/onboarding.js +616 -0
- package/lib/public/js/components/usage-tab.js +528 -0
- package/lib/public/js/components/watchdog-tab.js +1 -1
- package/lib/public/js/lib/api.js +25 -1
- package/lib/public/js/lib/telegram-api.js +78 -0
- package/lib/public/js/lib/ui-settings.js +38 -0
- package/lib/public/setup.html +34 -30
- package/lib/server/alphaclaw-version.js +3 -3
- package/lib/server/constants.js +1 -0
- package/lib/server/onboarding/openclaw.js +15 -0
- package/lib/server/routes/auth.js +5 -1
- package/lib/server/routes/telegram.js +185 -60
- package/lib/server/routes/usage.js +133 -0
- package/lib/server/usage-db.js +570 -0
- package/lib/server.js +21 -1
- package/lib/setup/core-prompts/AGENTS.md +0 -101
- package/package.json +1 -1
- package/lib/public/js/components/telegram-workspace.js +0 -1365
package/bin/alphaclaw.js
CHANGED
|
@@ -11,6 +11,14 @@ const {
|
|
|
11
11
|
} = require("../lib/cli/git-sync");
|
|
12
12
|
const { buildSecretReplacements } = require("../lib/server/helpers");
|
|
13
13
|
|
|
14
|
+
const kUsageTrackerPluginPath = path.resolve(
|
|
15
|
+
__dirname,
|
|
16
|
+
"..",
|
|
17
|
+
"lib",
|
|
18
|
+
"plugin",
|
|
19
|
+
"usage-tracker",
|
|
20
|
+
);
|
|
21
|
+
|
|
14
22
|
// ---------------------------------------------------------------------------
|
|
15
23
|
// Parse CLI flags
|
|
16
24
|
// ---------------------------------------------------------------------------
|
|
@@ -772,6 +780,8 @@ if (fs.existsSync(configPath)) {
|
|
|
772
780
|
const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
|
|
773
781
|
if (!cfg.channels) cfg.channels = {};
|
|
774
782
|
if (!cfg.plugins) cfg.plugins = {};
|
|
783
|
+
if (!cfg.plugins.load) cfg.plugins.load = {};
|
|
784
|
+
if (!Array.isArray(cfg.plugins.load.paths)) cfg.plugins.load.paths = [];
|
|
775
785
|
if (!cfg.plugins.entries) cfg.plugins.entries = {};
|
|
776
786
|
let changed = false;
|
|
777
787
|
|
|
@@ -798,6 +808,14 @@ if (fs.existsSync(configPath)) {
|
|
|
798
808
|
console.log("[alphaclaw] Discord added");
|
|
799
809
|
changed = true;
|
|
800
810
|
}
|
|
811
|
+
if (!cfg.plugins.load.paths.includes(kUsageTrackerPluginPath)) {
|
|
812
|
+
cfg.plugins.load.paths.push(kUsageTrackerPluginPath);
|
|
813
|
+
changed = true;
|
|
814
|
+
}
|
|
815
|
+
if (cfg.plugins.entries["usage-tracker"]?.enabled !== true) {
|
|
816
|
+
cfg.plugins.entries["usage-tracker"] = { enabled: true };
|
|
817
|
+
changed = true;
|
|
818
|
+
}
|
|
801
819
|
|
|
802
820
|
if (changed) {
|
|
803
821
|
let content = JSON.stringify(cfg, null, 2);
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const os = require("os");
|
|
3
|
+
const path = require("path");
|
|
4
|
+
const { DatabaseSync } = require("node:sqlite");
|
|
5
|
+
|
|
6
|
+
const kPluginId = "usage-tracker";
|
|
7
|
+
const kFallbackRootDir = path.join(os.homedir(), ".alphaclaw");
|
|
8
|
+
|
|
9
|
+
const coerceCount = (value) => {
|
|
10
|
+
const parsed = Number.parseInt(String(value ?? 0), 10);
|
|
11
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const resolveRootDir = () =>
|
|
15
|
+
process.env.ALPHACLAW_ROOT_DIR ||
|
|
16
|
+
process.env.OPENCLAW_HOME ||
|
|
17
|
+
process.env.OPENCLAW_ROOT_DIR ||
|
|
18
|
+
kFallbackRootDir;
|
|
19
|
+
|
|
20
|
+
const safeAlterTable = (database, sql) => {
|
|
21
|
+
try {
|
|
22
|
+
database.exec(sql);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
const message = String(err?.message || "").toLowerCase();
|
|
25
|
+
if (!message.includes("duplicate column name")) throw err;
|
|
26
|
+
}
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const ensureSchema = (database) => {
|
|
30
|
+
database.exec("PRAGMA journal_mode=WAL;");
|
|
31
|
+
database.exec("PRAGMA synchronous=NORMAL;");
|
|
32
|
+
database.exec("PRAGMA busy_timeout=5000;");
|
|
33
|
+
database.exec(`
|
|
34
|
+
CREATE TABLE IF NOT EXISTS usage_events (
|
|
35
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
36
|
+
timestamp INTEGER NOT NULL,
|
|
37
|
+
session_id TEXT,
|
|
38
|
+
session_key TEXT,
|
|
39
|
+
run_id TEXT,
|
|
40
|
+
provider TEXT NOT NULL,
|
|
41
|
+
model TEXT NOT NULL,
|
|
42
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
43
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
44
|
+
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
|
45
|
+
cache_write_tokens INTEGER NOT NULL DEFAULT 0,
|
|
46
|
+
total_tokens INTEGER NOT NULL DEFAULT 0
|
|
47
|
+
);
|
|
48
|
+
`);
|
|
49
|
+
database.exec(`
|
|
50
|
+
CREATE INDEX IF NOT EXISTS idx_usage_events_ts
|
|
51
|
+
ON usage_events(timestamp DESC);
|
|
52
|
+
`);
|
|
53
|
+
database.exec(`
|
|
54
|
+
CREATE INDEX IF NOT EXISTS idx_usage_events_session
|
|
55
|
+
ON usage_events(session_id);
|
|
56
|
+
`);
|
|
57
|
+
safeAlterTable(
|
|
58
|
+
database,
|
|
59
|
+
"ALTER TABLE usage_events ADD COLUMN session_key TEXT;",
|
|
60
|
+
);
|
|
61
|
+
database.exec(`
|
|
62
|
+
CREATE INDEX IF NOT EXISTS idx_usage_events_session_key
|
|
63
|
+
ON usage_events(session_key);
|
|
64
|
+
`);
|
|
65
|
+
database.exec(`
|
|
66
|
+
CREATE TABLE IF NOT EXISTS usage_daily (
|
|
67
|
+
date TEXT NOT NULL,
|
|
68
|
+
model TEXT NOT NULL,
|
|
69
|
+
provider TEXT,
|
|
70
|
+
input_tokens INTEGER NOT NULL DEFAULT 0,
|
|
71
|
+
output_tokens INTEGER NOT NULL DEFAULT 0,
|
|
72
|
+
cache_read_tokens INTEGER NOT NULL DEFAULT 0,
|
|
73
|
+
cache_write_tokens INTEGER NOT NULL DEFAULT 0,
|
|
74
|
+
total_tokens INTEGER NOT NULL DEFAULT 0,
|
|
75
|
+
turn_count INTEGER NOT NULL DEFAULT 0,
|
|
76
|
+
PRIMARY KEY (date, model)
|
|
77
|
+
);
|
|
78
|
+
`);
|
|
79
|
+
database.exec(`
|
|
80
|
+
CREATE TABLE IF NOT EXISTS tool_events (
|
|
81
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
82
|
+
timestamp INTEGER NOT NULL,
|
|
83
|
+
session_id TEXT,
|
|
84
|
+
session_key TEXT,
|
|
85
|
+
tool_name TEXT NOT NULL,
|
|
86
|
+
success INTEGER NOT NULL DEFAULT 1,
|
|
87
|
+
duration_ms INTEGER
|
|
88
|
+
);
|
|
89
|
+
`);
|
|
90
|
+
database.exec(`
|
|
91
|
+
CREATE INDEX IF NOT EXISTS idx_tool_events_session
|
|
92
|
+
ON tool_events(session_id);
|
|
93
|
+
`);
|
|
94
|
+
safeAlterTable(
|
|
95
|
+
database,
|
|
96
|
+
"ALTER TABLE tool_events ADD COLUMN session_key TEXT;",
|
|
97
|
+
);
|
|
98
|
+
database.exec(`
|
|
99
|
+
CREATE INDEX IF NOT EXISTS idx_tool_events_session_key
|
|
100
|
+
ON tool_events(session_key);
|
|
101
|
+
`);
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const createPlugin = () => {
|
|
105
|
+
let database = null;
|
|
106
|
+
let dbPath = "";
|
|
107
|
+
let insertUsageEventStmt = null;
|
|
108
|
+
let upsertUsageDailyStmt = null;
|
|
109
|
+
let insertToolEventStmt = null;
|
|
110
|
+
|
|
111
|
+
const getDatabase = () => {
|
|
112
|
+
if (database) return database;
|
|
113
|
+
const rootDir = resolveRootDir();
|
|
114
|
+
const dbDir = path.join(rootDir, "db");
|
|
115
|
+
fs.mkdirSync(dbDir, { recursive: true });
|
|
116
|
+
dbPath = path.join(dbDir, "usage.db");
|
|
117
|
+
database = new DatabaseSync(dbPath);
|
|
118
|
+
ensureSchema(database);
|
|
119
|
+
insertUsageEventStmt = database.prepare(`
|
|
120
|
+
INSERT INTO usage_events (
|
|
121
|
+
timestamp,
|
|
122
|
+
session_id,
|
|
123
|
+
session_key,
|
|
124
|
+
run_id,
|
|
125
|
+
provider,
|
|
126
|
+
model,
|
|
127
|
+
input_tokens,
|
|
128
|
+
output_tokens,
|
|
129
|
+
cache_read_tokens,
|
|
130
|
+
cache_write_tokens,
|
|
131
|
+
total_tokens
|
|
132
|
+
) VALUES (
|
|
133
|
+
$timestamp,
|
|
134
|
+
$session_id,
|
|
135
|
+
$session_key,
|
|
136
|
+
$run_id,
|
|
137
|
+
$provider,
|
|
138
|
+
$model,
|
|
139
|
+
$input_tokens,
|
|
140
|
+
$output_tokens,
|
|
141
|
+
$cache_read_tokens,
|
|
142
|
+
$cache_write_tokens,
|
|
143
|
+
$total_tokens
|
|
144
|
+
)
|
|
145
|
+
`);
|
|
146
|
+
upsertUsageDailyStmt = database.prepare(`
|
|
147
|
+
INSERT INTO usage_daily (
|
|
148
|
+
date,
|
|
149
|
+
model,
|
|
150
|
+
provider,
|
|
151
|
+
input_tokens,
|
|
152
|
+
output_tokens,
|
|
153
|
+
cache_read_tokens,
|
|
154
|
+
cache_write_tokens,
|
|
155
|
+
total_tokens,
|
|
156
|
+
turn_count
|
|
157
|
+
) VALUES (
|
|
158
|
+
$date,
|
|
159
|
+
$model,
|
|
160
|
+
$provider,
|
|
161
|
+
$input_tokens,
|
|
162
|
+
$output_tokens,
|
|
163
|
+
$cache_read_tokens,
|
|
164
|
+
$cache_write_tokens,
|
|
165
|
+
$total_tokens,
|
|
166
|
+
1
|
|
167
|
+
)
|
|
168
|
+
ON CONFLICT(date, model) DO UPDATE SET
|
|
169
|
+
provider = COALESCE(excluded.provider, usage_daily.provider),
|
|
170
|
+
input_tokens = usage_daily.input_tokens + excluded.input_tokens,
|
|
171
|
+
output_tokens = usage_daily.output_tokens + excluded.output_tokens,
|
|
172
|
+
cache_read_tokens = usage_daily.cache_read_tokens + excluded.cache_read_tokens,
|
|
173
|
+
cache_write_tokens = usage_daily.cache_write_tokens + excluded.cache_write_tokens,
|
|
174
|
+
total_tokens = usage_daily.total_tokens + excluded.total_tokens,
|
|
175
|
+
turn_count = usage_daily.turn_count + 1
|
|
176
|
+
`);
|
|
177
|
+
insertToolEventStmt = database.prepare(`
|
|
178
|
+
INSERT INTO tool_events (
|
|
179
|
+
timestamp,
|
|
180
|
+
session_id,
|
|
181
|
+
session_key,
|
|
182
|
+
tool_name,
|
|
183
|
+
success,
|
|
184
|
+
duration_ms
|
|
185
|
+
) VALUES (
|
|
186
|
+
$timestamp,
|
|
187
|
+
$session_id,
|
|
188
|
+
$session_key,
|
|
189
|
+
$tool_name,
|
|
190
|
+
$success,
|
|
191
|
+
$duration_ms
|
|
192
|
+
)
|
|
193
|
+
`);
|
|
194
|
+
return database;
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const writeUsageEvent = (event, ctx, logger) => {
|
|
198
|
+
const usage = event?.usage ?? {};
|
|
199
|
+
const timestamp = Date.now();
|
|
200
|
+
const date = new Date(timestamp).toISOString().slice(0, 10);
|
|
201
|
+
const inputTokens = coerceCount(usage.input);
|
|
202
|
+
const outputTokens = coerceCount(usage.output);
|
|
203
|
+
const cacheReadTokens = coerceCount(usage.cacheRead);
|
|
204
|
+
const cacheWriteTokens = coerceCount(usage.cacheWrite);
|
|
205
|
+
const fallbackTotal = inputTokens + outputTokens + cacheReadTokens + cacheWriteTokens;
|
|
206
|
+
const totalTokens = coerceCount(usage.total) || fallbackTotal;
|
|
207
|
+
if (totalTokens <= 0) return;
|
|
208
|
+
getDatabase();
|
|
209
|
+
insertUsageEventStmt.run({
|
|
210
|
+
$timestamp: timestamp,
|
|
211
|
+
$session_id: String(event?.sessionId || ctx?.sessionId || ""),
|
|
212
|
+
$session_key: String(ctx?.sessionKey || ""),
|
|
213
|
+
$run_id: String(event?.runId || ""),
|
|
214
|
+
$provider: String(event?.provider || "unknown"),
|
|
215
|
+
$model: String(event?.model || "unknown"),
|
|
216
|
+
$input_tokens: inputTokens,
|
|
217
|
+
$output_tokens: outputTokens,
|
|
218
|
+
$cache_read_tokens: cacheReadTokens,
|
|
219
|
+
$cache_write_tokens: cacheWriteTokens,
|
|
220
|
+
$total_tokens: totalTokens,
|
|
221
|
+
});
|
|
222
|
+
upsertUsageDailyStmt.run({
|
|
223
|
+
$date: date,
|
|
224
|
+
$model: String(event?.model || "unknown"),
|
|
225
|
+
$provider: String(event?.provider || "unknown"),
|
|
226
|
+
$input_tokens: inputTokens,
|
|
227
|
+
$output_tokens: outputTokens,
|
|
228
|
+
$cache_read_tokens: cacheReadTokens,
|
|
229
|
+
$cache_write_tokens: cacheWriteTokens,
|
|
230
|
+
$total_tokens: totalTokens,
|
|
231
|
+
});
|
|
232
|
+
if (logger?.debug) {
|
|
233
|
+
logger.debug(
|
|
234
|
+
`[${kPluginId}] usage event recorded model=${String(event?.model || "unknown")} total=${totalTokens}`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const deriveToolSuccess = (event) => {
|
|
240
|
+
const message = event?.message;
|
|
241
|
+
if (!message || typeof message !== "object") {
|
|
242
|
+
return event?.error ? 0 : 1;
|
|
243
|
+
}
|
|
244
|
+
if (message?.isError === true) return 0;
|
|
245
|
+
if (message?.ok === false) return 0;
|
|
246
|
+
if (typeof message?.error === "string" && message.error.trim()) return 0;
|
|
247
|
+
return 1;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const writeToolEvent = (event, ctx) => {
|
|
251
|
+
const toolName = String(event?.toolName || "").trim();
|
|
252
|
+
if (!toolName) return;
|
|
253
|
+
const sessionKey = String(ctx?.sessionKey || "").trim();
|
|
254
|
+
const sessionId = String(ctx?.sessionId || "").trim();
|
|
255
|
+
if (!sessionKey && !sessionId) return;
|
|
256
|
+
getDatabase();
|
|
257
|
+
insertToolEventStmt.run({
|
|
258
|
+
$timestamp: Date.now(),
|
|
259
|
+
$session_id: sessionId,
|
|
260
|
+
$session_key: sessionKey,
|
|
261
|
+
$tool_name: toolName,
|
|
262
|
+
$success: deriveToolSuccess(event),
|
|
263
|
+
$duration_ms: coerceCount(event?.durationMs) || null,
|
|
264
|
+
});
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
id: kPluginId,
|
|
269
|
+
name: "AlphaClaw Usage Tracker",
|
|
270
|
+
description: "Captures LLM and tool usage into SQLite for Usage UI",
|
|
271
|
+
register: (api) => {
|
|
272
|
+
const logger = api?.logger;
|
|
273
|
+
try {
|
|
274
|
+
getDatabase();
|
|
275
|
+
logger?.info?.(`[${kPluginId}] initialized db=${dbPath}`);
|
|
276
|
+
} catch (err) {
|
|
277
|
+
logger?.error?.(`[${kPluginId}] failed to initialize database: ${err?.message || err}`);
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
api.on("llm_output", (event, ctx) => {
|
|
281
|
+
try {
|
|
282
|
+
writeUsageEvent(event, ctx, logger);
|
|
283
|
+
} catch (err) {
|
|
284
|
+
logger?.error?.(`[${kPluginId}] llm_output write error: ${err?.message || err}`);
|
|
285
|
+
}
|
|
286
|
+
});
|
|
287
|
+
api.on("tool_result_persist", (event, ctx) => {
|
|
288
|
+
try {
|
|
289
|
+
writeToolEvent(
|
|
290
|
+
{
|
|
291
|
+
...event,
|
|
292
|
+
toolName: String(event?.toolName || ctx?.toolName || ""),
|
|
293
|
+
durationMs: event?.durationMs,
|
|
294
|
+
},
|
|
295
|
+
ctx,
|
|
296
|
+
);
|
|
297
|
+
} catch (err) {
|
|
298
|
+
logger?.error?.(`[${kPluginId}] tool_result_persist write error: ${err?.message || err}`);
|
|
299
|
+
}
|
|
300
|
+
return {};
|
|
301
|
+
});
|
|
302
|
+
},
|
|
303
|
+
};
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
const plugin = createPlugin();
|
|
307
|
+
module.exports = plugin;
|
|
308
|
+
module.exports.default = plugin;
|
|
@@ -102,7 +102,33 @@
|
|
|
102
102
|
.file-tree-wrap {
|
|
103
103
|
width: 100%;
|
|
104
104
|
overflow-y: auto;
|
|
105
|
-
padding:
|
|
105
|
+
padding: 6px 0 8px;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.file-tree-search {
|
|
109
|
+
padding: 0 8px 6px;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.file-tree-search-input {
|
|
113
|
+
width: 100%;
|
|
114
|
+
height: 28px;
|
|
115
|
+
border-radius: 7px;
|
|
116
|
+
border: 1px solid var(--border);
|
|
117
|
+
background: rgba(255, 255, 255, 0.02);
|
|
118
|
+
color: var(--text);
|
|
119
|
+
font-size: 12px;
|
|
120
|
+
padding: 0 9px;
|
|
121
|
+
outline: none;
|
|
122
|
+
font-family: inherit;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.file-tree-search-input::placeholder {
|
|
126
|
+
color: var(--text-dim);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.file-tree-search-input:focus {
|
|
130
|
+
border-color: rgba(99, 235, 255, 0.45);
|
|
131
|
+
box-shadow: 0 0 0 1px rgba(99, 235, 255, 0.18);
|
|
106
132
|
}
|
|
107
133
|
|
|
108
134
|
.file-tree-wrap::-webkit-scrollbar {
|
|
@@ -153,6 +179,11 @@
|
|
|
153
179
|
color: var(--accent);
|
|
154
180
|
}
|
|
155
181
|
|
|
182
|
+
.tree-item > a.soft-active:not(.active) {
|
|
183
|
+
background: rgba(99, 235, 255, 0.06);
|
|
184
|
+
color: var(--text);
|
|
185
|
+
}
|
|
186
|
+
|
|
156
187
|
.tree-item > a.active::before {
|
|
157
188
|
content: '';
|
|
158
189
|
position: absolute;
|
|
@@ -317,6 +348,16 @@
|
|
|
317
348
|
flex: 1;
|
|
318
349
|
}
|
|
319
350
|
|
|
351
|
+
.file-viewer-preview-pill {
|
|
352
|
+
margin-right: 8px;
|
|
353
|
+
font-size: 11px;
|
|
354
|
+
color: var(--text-muted);
|
|
355
|
+
border: 1px solid var(--border);
|
|
356
|
+
border-radius: 999px;
|
|
357
|
+
padding: 3px 8px;
|
|
358
|
+
line-height: 1;
|
|
359
|
+
}
|
|
360
|
+
|
|
320
361
|
.file-viewer-tab {
|
|
321
362
|
display: flex;
|
|
322
363
|
align-items: center;
|
|
@@ -791,6 +832,15 @@
|
|
|
791
832
|
font-size: 12px;
|
|
792
833
|
}
|
|
793
834
|
|
|
835
|
+
.file-viewer-loading-shell {
|
|
836
|
+
flex: 1 1 auto;
|
|
837
|
+
min-height: 140px;
|
|
838
|
+
display: flex;
|
|
839
|
+
align-items: center;
|
|
840
|
+
justify-content: center;
|
|
841
|
+
color: var(--text-muted);
|
|
842
|
+
}
|
|
843
|
+
|
|
794
844
|
.file-viewer-state-error {
|
|
795
845
|
color: #f87171;
|
|
796
846
|
}
|
package/lib/public/css/shell.css
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/* ── App shell grid ─────────────────────────────── */
|
|
2
2
|
|
|
3
3
|
.app-shell {
|
|
4
|
+
--sidebar-width: 220px;
|
|
4
5
|
display: grid;
|
|
5
|
-
grid-template-columns:
|
|
6
|
+
grid-template-columns: var(--sidebar-width) 0px minmax(0, 1fr);
|
|
6
7
|
grid-template-rows: auto 1fr 24px;
|
|
7
8
|
height: 100vh;
|
|
8
9
|
position: relative;
|
|
@@ -286,6 +287,7 @@
|
|
|
286
287
|
|
|
287
288
|
@media (max-width: 768px) {
|
|
288
289
|
.app-shell {
|
|
290
|
+
--sidebar-width: 0px !important;
|
|
289
291
|
grid-template-columns: 1fr;
|
|
290
292
|
grid-template-rows: auto 1fr 24px;
|
|
291
293
|
}
|
package/lib/public/css/theme.css
CHANGED
|
@@ -451,3 +451,38 @@ textarea:focus {
|
|
|
451
451
|
animation-timing-function: linear;
|
|
452
452
|
}
|
|
453
453
|
}
|
|
454
|
+
|
|
455
|
+
/* Reusable segmented control (pill toggle). */
|
|
456
|
+
.ac-segmented-control {
|
|
457
|
+
display: inline-flex;
|
|
458
|
+
align-items: center;
|
|
459
|
+
border: 1px solid var(--panel-border-contrast);
|
|
460
|
+
border-radius: 8px;
|
|
461
|
+
overflow: hidden;
|
|
462
|
+
background: rgba(255, 255, 255, 0.02);
|
|
463
|
+
height: 28px;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
.ac-segmented-control-button {
|
|
467
|
+
border: 0;
|
|
468
|
+
background: transparent;
|
|
469
|
+
color: var(--text-muted);
|
|
470
|
+
font-family: inherit;
|
|
471
|
+
font-size: 12px;
|
|
472
|
+
letter-spacing: 0.03em;
|
|
473
|
+
height: 100%;
|
|
474
|
+
line-height: 1;
|
|
475
|
+
padding: 0 10px;
|
|
476
|
+
cursor: pointer;
|
|
477
|
+
transition: color 0.12s, background 0.12s;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
.ac-segmented-control-button:hover {
|
|
481
|
+
color: var(--text);
|
|
482
|
+
background: rgba(255, 255, 255, 0.03);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
.ac-segmented-control-button.active {
|
|
486
|
+
color: var(--accent);
|
|
487
|
+
background: var(--bg-active);
|
|
488
|
+
}
|
package/lib/public/js/app.js
CHANGED
|
@@ -34,7 +34,7 @@ import { Welcome } from "./components/welcome.js";
|
|
|
34
34
|
import { Envars } from "./components/envars.js";
|
|
35
35
|
import { Webhooks } from "./components/webhooks.js";
|
|
36
36
|
import { ToastContainer, showToast } from "./components/toast.js";
|
|
37
|
-
import { TelegramWorkspace } from "./components/telegram-workspace.js";
|
|
37
|
+
import { TelegramWorkspace } from "./components/telegram-workspace/index.js";
|
|
38
38
|
import { ChevronDownIcon } from "./components/icons.js";
|
|
39
39
|
import { UpdateActionButton } from "./components/update-action-button.js";
|
|
40
40
|
import { GlobalRestartBanner } from "./components/global-restart-banner.js";
|
|
@@ -42,31 +42,19 @@ import { LoadingSpinner } from "./components/loading-spinner.js";
|
|
|
42
42
|
import { WatchdogTab } from "./components/watchdog-tab.js";
|
|
43
43
|
import { FileViewer } from "./components/file-viewer.js";
|
|
44
44
|
import { AppSidebar } from "./components/sidebar.js";
|
|
45
|
+
import { UsageTab } from "./components/usage-tab.js";
|
|
46
|
+
import { readUiSettings, writeUiSettings } from "./lib/ui-settings.js";
|
|
45
47
|
const html = htm.bind(h);
|
|
46
48
|
const kDefaultUiTab = "general";
|
|
47
|
-
const kUiSettingsStorageKey = "alphaclaw.uiSettings";
|
|
48
|
-
const kLegacyUiSettingsStorageKey = "alphaclawUiSettings";
|
|
49
49
|
const kDefaultSidebarWidthPx = 220;
|
|
50
50
|
const kSidebarMinWidthPx = 180;
|
|
51
51
|
const kSidebarMaxWidthPx = 460;
|
|
52
52
|
const kBrowseLastPathUiSettingKey = "browseLastPath";
|
|
53
|
+
const kLastMenuRouteUiSettingKey = "lastMenuRoute";
|
|
53
54
|
|
|
54
55
|
const clampSidebarWidth = (value) =>
|
|
55
56
|
Math.max(kSidebarMinWidthPx, Math.min(kSidebarMaxWidthPx, value));
|
|
56
57
|
|
|
57
|
-
const readUiSettings = () => {
|
|
58
|
-
try {
|
|
59
|
-
const raw =
|
|
60
|
-
window.localStorage.getItem(kUiSettingsStorageKey) ||
|
|
61
|
-
window.localStorage.getItem(kLegacyUiSettingsStorageKey);
|
|
62
|
-
if (!raw) return {};
|
|
63
|
-
const parsed = JSON.parse(raw);
|
|
64
|
-
return parsed && typeof parsed === "object" ? parsed : {};
|
|
65
|
-
} catch {
|
|
66
|
-
return {};
|
|
67
|
-
}
|
|
68
|
-
};
|
|
69
|
-
|
|
70
58
|
const getHashPath = () => {
|
|
71
59
|
const hash = window.location.hash.replace(/^#/, "");
|
|
72
60
|
if (!hash) return `/${kDefaultUiTab}`;
|
|
@@ -407,7 +395,20 @@ const App = () => {
|
|
|
407
395
|
? settings[kBrowseLastPathUiSettingKey]
|
|
408
396
|
: "";
|
|
409
397
|
});
|
|
398
|
+
const [lastMenuRoute, setLastMenuRoute] = useState(() => {
|
|
399
|
+
const settings = readUiSettings();
|
|
400
|
+
const storedRoute = settings[kLastMenuRouteUiSettingKey];
|
|
401
|
+
if (
|
|
402
|
+
typeof storedRoute === "string" &&
|
|
403
|
+
storedRoute.startsWith("/") &&
|
|
404
|
+
!storedRoute.startsWith("/browse")
|
|
405
|
+
) {
|
|
406
|
+
return storedRoute;
|
|
407
|
+
}
|
|
408
|
+
return `/${kDefaultUiTab}`;
|
|
409
|
+
});
|
|
410
410
|
const [isResizingSidebar, setIsResizingSidebar] = useState(false);
|
|
411
|
+
const [browsePreviewPath, setBrowsePreviewPath] = useState("");
|
|
411
412
|
const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
|
|
412
413
|
const [mobileTopbarScrolled, setMobileTopbarScrolled] = useState(false);
|
|
413
414
|
const [restartRequired, setRestartRequired] = useState(false);
|
|
@@ -603,6 +604,7 @@ const App = () => {
|
|
|
603
604
|
setMobileSidebarOpen(false);
|
|
604
605
|
};
|
|
605
606
|
const navigateToBrowseFile = (relativePath) => {
|
|
607
|
+
setBrowsePreviewPath("");
|
|
606
608
|
setLocation(buildBrowseRoute(relativePath));
|
|
607
609
|
setMobileSidebarOpen(false);
|
|
608
610
|
};
|
|
@@ -618,7 +620,8 @@ const App = () => {
|
|
|
618
620
|
const handleSelectSidebarTab = (nextTab) => {
|
|
619
621
|
setSidebarTab(nextTab);
|
|
620
622
|
if (nextTab === "menu" && location.startsWith("/browse")) {
|
|
621
|
-
|
|
623
|
+
setBrowsePreviewPath("");
|
|
624
|
+
setLocation(lastMenuRoute || `/${kDefaultUiTab}`);
|
|
622
625
|
return;
|
|
623
626
|
}
|
|
624
627
|
if (nextTab === "browse" && !location.startsWith("/browse")) {
|
|
@@ -642,10 +645,16 @@ const App = () => {
|
|
|
642
645
|
|
|
643
646
|
const kNavSections = [
|
|
644
647
|
{
|
|
645
|
-
label: "
|
|
648
|
+
label: "Setup",
|
|
646
649
|
items: [
|
|
647
650
|
{ id: "general", label: "General" },
|
|
651
|
+
],
|
|
652
|
+
},
|
|
653
|
+
{
|
|
654
|
+
label: "Monitoring",
|
|
655
|
+
items: [
|
|
648
656
|
{ id: "watchdog", label: "Watchdog" },
|
|
657
|
+
{ id: "usage", label: "Usage" },
|
|
649
658
|
],
|
|
650
659
|
},
|
|
651
660
|
{
|
|
@@ -681,6 +690,8 @@ const App = () => {
|
|
|
681
690
|
? "providers"
|
|
682
691
|
: location.startsWith("/watchdog")
|
|
683
692
|
? "watchdog"
|
|
693
|
+
: location.startsWith("/usage")
|
|
694
|
+
? "usage"
|
|
684
695
|
: location.startsWith("/envars")
|
|
685
696
|
? "envars"
|
|
686
697
|
: location.startsWith("/webhooks")
|
|
@@ -695,6 +706,19 @@ const App = () => {
|
|
|
695
706
|
});
|
|
696
707
|
}, [location]);
|
|
697
708
|
|
|
709
|
+
useEffect(() => {
|
|
710
|
+
if (location.startsWith("/browse")) return;
|
|
711
|
+
setBrowsePreviewPath("");
|
|
712
|
+
}, [location]);
|
|
713
|
+
|
|
714
|
+
useEffect(() => {
|
|
715
|
+
if (location.startsWith("/browse")) return;
|
|
716
|
+
if (location === "/telegram") return;
|
|
717
|
+
setLastMenuRoute((currentRoute) =>
|
|
718
|
+
currentRoute === location ? currentRoute : location,
|
|
719
|
+
);
|
|
720
|
+
}, [location]);
|
|
721
|
+
|
|
698
722
|
useEffect(() => {
|
|
699
723
|
if (!isBrowseRoute) return;
|
|
700
724
|
if (!selectedBrowsePath) return;
|
|
@@ -707,10 +731,9 @@ const App = () => {
|
|
|
707
731
|
const settings = readUiSettings();
|
|
708
732
|
settings.sidebarWidthPx = sidebarWidthPx;
|
|
709
733
|
settings[kBrowseLastPathUiSettingKey] = lastBrowsePath;
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
}, [sidebarWidthPx, lastBrowsePath]);
|
|
734
|
+
settings[kLastMenuRouteUiSettingKey] = lastMenuRoute;
|
|
735
|
+
writeUiSettings(settings);
|
|
736
|
+
}, [sidebarWidthPx, lastBrowsePath, lastMenuRoute]);
|
|
714
737
|
|
|
715
738
|
const resizeSidebarWithClientX = useCallback((clientX) => {
|
|
716
739
|
const shellElement = appShellRef.current;
|
|
@@ -759,7 +782,7 @@ const App = () => {
|
|
|
759
782
|
<div
|
|
760
783
|
class="app-shell"
|
|
761
784
|
ref=${appShellRef}
|
|
762
|
-
style=${{
|
|
785
|
+
style=${{ "--sidebar-width": `${sidebarWidthPx}px` }}
|
|
763
786
|
>
|
|
764
787
|
<${GlobalRestartBanner}
|
|
765
788
|
visible=${restartRequired}
|
|
@@ -780,6 +803,7 @@ const App = () => {
|
|
|
780
803
|
onSelectNavItem=${handleSelectNavItem}
|
|
781
804
|
selectedBrowsePath=${selectedBrowsePath}
|
|
782
805
|
onSelectBrowseFile=${navigateToBrowseFile}
|
|
806
|
+
onPreviewBrowseFile=${setBrowsePreviewPath}
|
|
783
807
|
acHasUpdate=${acHasUpdate}
|
|
784
808
|
acLatest=${acLatest}
|
|
785
809
|
acDismissed=${acDismissed}
|
|
@@ -824,7 +848,11 @@ const App = () => {
|
|
|
824
848
|
${isBrowseRoute
|
|
825
849
|
? html`
|
|
826
850
|
<${FileViewer}
|
|
827
|
-
filePath=${selectedBrowsePath}
|
|
851
|
+
filePath=${browsePreviewPath || selectedBrowsePath}
|
|
852
|
+
isPreviewOnly=${Boolean(
|
|
853
|
+
browsePreviewPath &&
|
|
854
|
+
browsePreviewPath !== selectedBrowsePath,
|
|
855
|
+
)}
|
|
828
856
|
/>
|
|
829
857
|
`
|
|
830
858
|
: html`
|
|
@@ -867,6 +895,27 @@ const App = () => {
|
|
|
867
895
|
/>
|
|
868
896
|
</div>
|
|
869
897
|
</${Route}>
|
|
898
|
+
<${Route} path="/usage/:sessionId">
|
|
899
|
+
${(params) => html`
|
|
900
|
+
<div class="pt-4">
|
|
901
|
+
<${UsageTab}
|
|
902
|
+
sessionId=${decodeURIComponent(params.sessionId || "")}
|
|
903
|
+
onSelectSession=${(id) =>
|
|
904
|
+
setLocation(`/usage/${encodeURIComponent(String(id || ""))}`)}
|
|
905
|
+
onBackToSessions=${() => setLocation("/usage")}
|
|
906
|
+
/>
|
|
907
|
+
</div>
|
|
908
|
+
`}
|
|
909
|
+
</${Route}>
|
|
910
|
+
<${Route} path="/usage">
|
|
911
|
+
<div class="pt-4">
|
|
912
|
+
<${UsageTab}
|
|
913
|
+
onSelectSession=${(id) =>
|
|
914
|
+
setLocation(`/usage/${encodeURIComponent(String(id || ""))}`)}
|
|
915
|
+
onBackToSessions=${() => setLocation("/usage")}
|
|
916
|
+
/>
|
|
917
|
+
</div>
|
|
918
|
+
</${Route}>
|
|
870
919
|
<${Route} path="/envars">
|
|
871
920
|
<div class="pt-4">
|
|
872
921
|
<${Envars} onRestartRequired=${setRestartRequired} />
|