mirror of
https://github.com/cline/cline.git
synced 2026-09-01 15:11:04 +08:00
Add Hub Monitor UI to menubar example (#10688)
* add menubar hub monitor dashboard * docs: add menubar preview screenshot
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
### Preview
|
||||
|
||||

|
||||
|
||||
### Architecture Overview
|
||||
|
||||
```
|
||||
@@ -14,6 +18,12 @@ Menu Bar Sidecar (apps/examples/menubar/sidecar/index.ts) ← TypeScript/Bun pr
|
||||
│ JSON lines on stdout: hub_state / notification / ready
|
||||
▼
|
||||
Rust Tauri App (apps/examples/menubar/src-tauri/src/main.rs)
|
||||
│
|
||||
├── Hub Monitor Window (ui/index.html)
|
||||
│ ● Live hub status, uptime, clients, sessions
|
||||
│ ● Running session tracker and inspector
|
||||
│ ● Recent events and background-session launcher
|
||||
│
|
||||
│
|
||||
├── System Tray Icon with dynamic menu
|
||||
│ ● Hub Connected — 3 clients, 2 sessions
|
||||
@@ -24,3 +34,11 @@ Rust Tauri App (apps/examples/menubar/src-tauri/src/main.rs)
|
||||
│
|
||||
└── Logs notifications to stderr (with severity)
|
||||
```
|
||||
|
||||
### Dev Commands
|
||||
|
||||
From `sdk/apps/examples/menubar/`:
|
||||
|
||||
- `bun run dev:ui` - run only the Hub Monitor UI at `http://127.0.0.1:3466/` with preview data
|
||||
- `bun run dev` - run the full Tauri app with the real hub sidecar
|
||||
- `bun run typecheck` - TypeScript check
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 71 KiB |
@@ -4,6 +4,7 @@
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev:ui": "python3 -m http.server 3466 --bind 127.0.0.1 --directory ui",
|
||||
"dev": "tauri dev",
|
||||
"build": "tauri build",
|
||||
"build:sidecar:bin": "bun run scripts/build-sidecar-bin.ts",
|
||||
|
||||
@@ -23,8 +23,18 @@ interface TrackedSession {
|
||||
sessionId: string;
|
||||
status: string;
|
||||
workspaceRoot: string;
|
||||
cwd?: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
createdByClientId?: string;
|
||||
title?: string;
|
||||
provider?: string;
|
||||
model?: string;
|
||||
prompt?: string;
|
||||
inputTokens?: number;
|
||||
outputTokens?: number;
|
||||
totalCost?: number;
|
||||
agentCount?: number;
|
||||
}
|
||||
|
||||
interface ClientSummary {
|
||||
@@ -33,6 +43,33 @@ interface ClientSummary {
|
||||
sessionCount: number;
|
||||
}
|
||||
|
||||
interface SessionSummary {
|
||||
sessionId: string;
|
||||
title: string;
|
||||
status: string;
|
||||
workspaceRoot: string;
|
||||
workspaceName: string;
|
||||
cwd?: string;
|
||||
model?: string;
|
||||
provider?: string;
|
||||
createdAt: number;
|
||||
updatedAt: number;
|
||||
createdByClientId?: string;
|
||||
prompt?: string;
|
||||
inputTokens?: number;
|
||||
outputTokens?: number;
|
||||
totalCost?: number;
|
||||
agentCount: number;
|
||||
}
|
||||
|
||||
interface EventRecord {
|
||||
id: string;
|
||||
title: string;
|
||||
body: string;
|
||||
severity: "info" | "success" | "warn" | "error";
|
||||
timestamp: number;
|
||||
}
|
||||
|
||||
type ClientSummaryGroup = {
|
||||
label: string;
|
||||
name: string;
|
||||
@@ -67,8 +104,9 @@ interface ProviderLaunchAuth {
|
||||
}
|
||||
|
||||
interface SidecarCommand {
|
||||
type: "new_chat" | "shutdown_hub";
|
||||
type: "new_chat" | "shutdown_hub" | "abort_session";
|
||||
prompt?: string;
|
||||
sessionId?: string;
|
||||
}
|
||||
|
||||
function isVisibleClient(clientType: string): boolean {
|
||||
@@ -103,6 +141,70 @@ function emitNotification(
|
||||
});
|
||||
}
|
||||
|
||||
function asRecord(value: unknown): Record<string, unknown> | undefined {
|
||||
return value && typeof value === "object"
|
||||
? (value as Record<string, unknown>)
|
||||
: undefined;
|
||||
}
|
||||
|
||||
function asString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
}
|
||||
|
||||
function asNumber(value: unknown): number | undefined {
|
||||
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
||||
}
|
||||
|
||||
function basename(value: string | undefined): string {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed) {
|
||||
return "workspace";
|
||||
}
|
||||
const parts = trimmed.split(/[\\/]+/).filter(Boolean);
|
||||
return parts.at(-1) ?? trimmed;
|
||||
}
|
||||
|
||||
function shortSessionId(sessionId: string): string {
|
||||
return sessionId.length > 10 ? sessionId.slice(0, 10) : sessionId;
|
||||
}
|
||||
|
||||
function pushEvent(
|
||||
events: EventRecord[],
|
||||
title: string,
|
||||
body: string,
|
||||
severity: EventRecord["severity"] = "info",
|
||||
timestamp = Date.now(),
|
||||
): void {
|
||||
events.unshift({
|
||||
id: `${timestamp}-${events.length}-${title}`,
|
||||
title,
|
||||
body,
|
||||
severity,
|
||||
timestamp,
|
||||
});
|
||||
if (events.length > 30) {
|
||||
events.length = 30;
|
||||
}
|
||||
}
|
||||
|
||||
function metadataFor(
|
||||
session: SessionRecord | Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
return asRecord(session.metadata) ?? {};
|
||||
}
|
||||
|
||||
function usageFor(
|
||||
session: SessionRecord | Record<string, unknown>,
|
||||
): Record<string, unknown> {
|
||||
return (
|
||||
asRecord(session.aggregateUsage) ??
|
||||
asRecord(session.usage) ??
|
||||
asRecord(metadataFor(session).aggregateUsage) ??
|
||||
asRecord(metadataFor(session).usage) ??
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
function formatUptime(ms: number): string {
|
||||
const totalSeconds = Math.max(0, Math.floor(ms / 1000));
|
||||
const days = Math.floor(totalSeconds / 86_400);
|
||||
@@ -169,6 +271,87 @@ function summarizeClient(client: TrackedClient): {
|
||||
};
|
||||
}
|
||||
|
||||
function sessionTitle(session: SessionRecord | Record<string, unknown>): string {
|
||||
const raw = session as Record<string, unknown>;
|
||||
const metadata = metadataFor(session);
|
||||
const title = asString(metadata.title);
|
||||
if (title) {
|
||||
return title;
|
||||
}
|
||||
const prompt = asString(raw.prompt) ?? asString(metadata.prompt);
|
||||
if (prompt) {
|
||||
return prompt.length > 34 ? `${prompt.slice(0, 31)}...` : prompt;
|
||||
}
|
||||
return basename(asString(raw.workspaceRoot) ?? asString(raw.cwd));
|
||||
}
|
||||
|
||||
function trackedSessionFrom(
|
||||
session: SessionRecord | Record<string, unknown>,
|
||||
): TrackedSession | undefined {
|
||||
const raw = session as Record<string, unknown>;
|
||||
const sessionId = asString(raw.sessionId);
|
||||
if (!sessionId) {
|
||||
return undefined;
|
||||
}
|
||||
const metadata = metadataFor(session);
|
||||
const usage = usageFor(session);
|
||||
const createdAt =
|
||||
asNumber(raw.createdAt) ??
|
||||
asNumber(raw.startedAt) ??
|
||||
asNumber(metadata.createdAt) ??
|
||||
Date.now();
|
||||
return {
|
||||
sessionId,
|
||||
status: asString(raw.status) ?? "running",
|
||||
workspaceRoot: asString(raw.workspaceRoot) ?? asString(raw.cwd) ?? "",
|
||||
cwd: asString(raw.cwd),
|
||||
createdAt,
|
||||
updatedAt:
|
||||
asNumber(raw.updatedAt) ??
|
||||
asNumber(raw.endedAt) ??
|
||||
asNumber(metadata.updatedAt) ??
|
||||
createdAt,
|
||||
createdByClientId: asString(raw.createdByClientId),
|
||||
title: sessionTitle(session),
|
||||
provider: asString(raw.provider) ?? asString(metadata.provider),
|
||||
model: asString(raw.model) ?? asString(metadata.model),
|
||||
prompt: asString(raw.prompt) ?? asString(metadata.prompt),
|
||||
inputTokens:
|
||||
asNumber(usage.inputTokens) ??
|
||||
asNumber(usage.input) ??
|
||||
asNumber(usage.totalInputTokens),
|
||||
outputTokens:
|
||||
asNumber(usage.outputTokens) ??
|
||||
asNumber(usage.output) ??
|
||||
asNumber(usage.totalOutputTokens),
|
||||
totalCost: asNumber(usage.totalCost) ?? asNumber(metadata.totalCost),
|
||||
agentCount: Array.isArray(raw.participants)
|
||||
? Math.max(1, raw.participants.length)
|
||||
: 1,
|
||||
};
|
||||
}
|
||||
|
||||
function toSessionSummary(session: TrackedSession): SessionSummary {
|
||||
return {
|
||||
sessionId: session.sessionId,
|
||||
title: session.title || basename(session.workspaceRoot),
|
||||
status: session.status,
|
||||
workspaceRoot: session.workspaceRoot,
|
||||
workspaceName: basename(session.workspaceRoot || session.cwd),
|
||||
cwd: session.cwd,
|
||||
model: session.model,
|
||||
provider: session.provider,
|
||||
createdAt: session.createdAt,
|
||||
updatedAt: session.updatedAt,
|
||||
createdByClientId: session.createdByClientId,
|
||||
prompt: session.prompt,
|
||||
inputTokens: session.inputTokens,
|
||||
outputTokens: session.outputTokens,
|
||||
totalCost: session.totalCost,
|
||||
agentCount: session.agentCount ?? 1,
|
||||
};
|
||||
}
|
||||
|
||||
function parseLastSessionContext(
|
||||
session: SessionRecord | Record<string, unknown> | undefined,
|
||||
): LastSessionContext | undefined {
|
||||
@@ -267,6 +450,7 @@ async function main(): Promise<void> {
|
||||
|
||||
const clients = new Map<string, TrackedClient>();
|
||||
const sessions = new Map<string, TrackedSession>();
|
||||
const events: EventRecord[] = [];
|
||||
let lastSessionContext: LastSessionContext | undefined;
|
||||
let hubStartedAt: string | undefined;
|
||||
|
||||
@@ -316,13 +500,10 @@ async function main(): Promise<void> {
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
sessions.set(session.sessionId, {
|
||||
sessionId: session.sessionId,
|
||||
status: session.status,
|
||||
workspaceRoot: session.workspaceRoot,
|
||||
createdAt: session.createdAt,
|
||||
createdByClientId: session.createdByClientId,
|
||||
});
|
||||
const tracked = trackedSessionFrom(session);
|
||||
if (tracked) {
|
||||
sessions.set(tracked.sessionId, tracked);
|
||||
}
|
||||
}
|
||||
const mostRecentContext = [...knownSessions]
|
||||
.sort((a, b) => b.updatedAt - a.updatedAt)
|
||||
@@ -331,6 +512,12 @@ async function main(): Promise<void> {
|
||||
if (mostRecentContext) {
|
||||
lastSessionContext = mostRecentContext;
|
||||
}
|
||||
pushEvent(
|
||||
events,
|
||||
"Hub monitor connected",
|
||||
`${knownClients.length} clients and ${knownSessions.length} sessions discovered`,
|
||||
"success",
|
||||
);
|
||||
};
|
||||
|
||||
function emitState(): void {
|
||||
@@ -378,6 +565,10 @@ async function main(): Promise<void> {
|
||||
clients: Array.from(clients.values()),
|
||||
sessions: Array.from(sessions.values()),
|
||||
clientSummaries,
|
||||
sessionSummaries: Array.from(sessions.values())
|
||||
.sort((a, b) => b.updatedAt - a.updatedAt)
|
||||
.map(toSessionSummary),
|
||||
events,
|
||||
lastWorkspaceRoot: lastSessionContext?.workspaceRoot,
|
||||
hubStartedAt,
|
||||
hubUptime: hubStartedAt
|
||||
@@ -455,6 +646,30 @@ async function main(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function abortBackgroundSession(sessionId: string): Promise<void> {
|
||||
const trimmedSessionId = sessionId.trim();
|
||||
if (!trimmedSessionId) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
await sessionClient.abortRuntimeSession(trimmedSessionId);
|
||||
pushEvent(
|
||||
events,
|
||||
"Session abort requested",
|
||||
`Requested stop for ${shortSessionId(trimmedSessionId)}`,
|
||||
"warn",
|
||||
);
|
||||
emitState();
|
||||
} catch (error) {
|
||||
emit({
|
||||
type: "notification",
|
||||
title: "Stop session failed",
|
||||
body: error instanceof Error ? error.message : String(error),
|
||||
severity: "error",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const stdin = createInterface({ input: process.stdin, terminal: false });
|
||||
stdin.on("line", (line) => {
|
||||
const trimmed = line.trim();
|
||||
@@ -476,10 +691,25 @@ async function main(): Promise<void> {
|
||||
if (command?.type === "shutdown_hub") {
|
||||
void shutdownHub();
|
||||
}
|
||||
if (command?.type === "abort_session") {
|
||||
if (typeof command.sessionId === "string") {
|
||||
void abortBackgroundSession(command.sessionId);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
uiClient.subscribeUI({
|
||||
onNotify(payload: HubUINotifyPayload) {
|
||||
pushEvent(
|
||||
events,
|
||||
payload.title,
|
||||
payload.body,
|
||||
payload.severity === "error"
|
||||
? "error"
|
||||
: payload.severity === "warning"
|
||||
? "warn"
|
||||
: "info",
|
||||
);
|
||||
emit({
|
||||
type: "notification",
|
||||
title: payload.title,
|
||||
@@ -510,13 +740,26 @@ async function main(): Promise<void> {
|
||||
: "unknown",
|
||||
connectedAt: Date.now(),
|
||||
});
|
||||
pushEvent(
|
||||
events,
|
||||
"Client connected",
|
||||
`${formatClientLabel(typeof payload.clientType === "string" ? payload.clientType : "unknown")} joined the hub`,
|
||||
"success",
|
||||
);
|
||||
emitState();
|
||||
},
|
||||
onClientDisconnected(payload) {
|
||||
const clientId =
|
||||
typeof payload.clientId === "string" ? payload.clientId : undefined;
|
||||
if (!clientId) return;
|
||||
const client = clients.get(clientId);
|
||||
clients.delete(clientId);
|
||||
pushEvent(
|
||||
events,
|
||||
"Client disconnected",
|
||||
`${client?.displayName ?? client?.clientType ?? clientId} left the hub`,
|
||||
"warn",
|
||||
);
|
||||
emitState();
|
||||
},
|
||||
onSessionCreated(payload) {
|
||||
@@ -543,19 +786,16 @@ async function main(): Promise<void> {
|
||||
emitState();
|
||||
return;
|
||||
}
|
||||
sessions.set(sessionId, {
|
||||
sessionId,
|
||||
status,
|
||||
workspaceRoot:
|
||||
typeof session.workspaceRoot === "string"
|
||||
? session.workspaceRoot
|
||||
: "",
|
||||
createdAt: Date.now(),
|
||||
createdByClientId:
|
||||
typeof session.createdByClientId === "string"
|
||||
? session.createdByClientId
|
||||
: undefined,
|
||||
});
|
||||
const tracked = trackedSessionFrom({ ...session, status });
|
||||
if (!tracked) return;
|
||||
sessions.set(sessionId, tracked);
|
||||
pushEvent(
|
||||
events,
|
||||
`Started session "${tracked.title ?? shortSessionId(sessionId)}"`,
|
||||
`${tracked.workspaceRoot || "workspace"} on ${tracked.model ?? "selected model"}`,
|
||||
"success",
|
||||
tracked.createdAt,
|
||||
);
|
||||
emitState();
|
||||
},
|
||||
onSessionUpdated(payload) {
|
||||
@@ -584,26 +824,42 @@ async function main(): Promise<void> {
|
||||
return;
|
||||
}
|
||||
if (existing) {
|
||||
const previousStatus = existing.status;
|
||||
existing.status = status;
|
||||
existing.updatedAt =
|
||||
asNumber(session.updatedAt) ?? asNumber(session.endedAt) ?? Date.now();
|
||||
existing.title = sessionTitle(session);
|
||||
existing.workspaceRoot =
|
||||
typeof session.workspaceRoot === "string"
|
||||
? session.workspaceRoot
|
||||
: existing.workspaceRoot;
|
||||
existing.cwd =
|
||||
typeof session.cwd === "string" ? session.cwd : existing.cwd;
|
||||
const metadata = metadataFor(session);
|
||||
existing.provider =
|
||||
asString(session.provider) ??
|
||||
asString(metadata.provider) ??
|
||||
existing.provider;
|
||||
existing.model =
|
||||
asString(session.model) ?? asString(metadata.model) ?? existing.model;
|
||||
existing.createdByClientId =
|
||||
typeof session.createdByClientId === "string"
|
||||
? session.createdByClientId
|
||||
: existing.createdByClientId;
|
||||
sessions.set(sessionId, existing);
|
||||
if (previousStatus !== status) {
|
||||
pushEvent(
|
||||
events,
|
||||
`Session ${status}`,
|
||||
`${existing.title ?? shortSessionId(sessionId)} changed from ${previousStatus}`,
|
||||
status === "running" ? "success" : status === "idle" ? "info" : "warn",
|
||||
);
|
||||
}
|
||||
} else {
|
||||
sessions.set(sessionId, {
|
||||
sessionId,
|
||||
status,
|
||||
workspaceRoot:
|
||||
typeof session.workspaceRoot === "string"
|
||||
? session.workspaceRoot
|
||||
: "",
|
||||
createdAt: Date.now(),
|
||||
createdByClientId:
|
||||
typeof session.createdByClientId === "string"
|
||||
? session.createdByClientId
|
||||
: undefined,
|
||||
});
|
||||
const tracked = trackedSessionFrom({ ...session, status });
|
||||
if (tracked) {
|
||||
sessions.set(sessionId, tracked);
|
||||
}
|
||||
}
|
||||
emitState();
|
||||
},
|
||||
@@ -617,13 +873,20 @@ async function main(): Promise<void> {
|
||||
? session.sessionId
|
||||
: typeof payload.sessionId === "string"
|
||||
? payload.sessionId
|
||||
: undefined;
|
||||
: undefined;
|
||||
if (!sessionId) return;
|
||||
const participantCount = Array.isArray(session?.participants)
|
||||
? session.participants.length
|
||||
: 0;
|
||||
if (participantCount <= 0) {
|
||||
const tracked = sessions.get(sessionId);
|
||||
sessions.delete(sessionId);
|
||||
pushEvent(
|
||||
events,
|
||||
"Session detached",
|
||||
`${tracked?.title ?? shortSessionId(sessionId)} has no active participants`,
|
||||
"warn",
|
||||
);
|
||||
emitState();
|
||||
}
|
||||
},
|
||||
|
||||
@@ -23,6 +23,8 @@ struct HubState {
|
||||
hub_uptime: Option<String>,
|
||||
last_error: Option<String>,
|
||||
client_summaries: Vec<ClientSummary>,
|
||||
session_summaries: Vec<SessionSummary>,
|
||||
events: Vec<HubEventRecord>,
|
||||
notifications: Vec<NotificationRecord>,
|
||||
}
|
||||
|
||||
@@ -34,6 +36,37 @@ struct ClientSummary {
|
||||
session_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct SessionSummary {
|
||||
session_id: String,
|
||||
title: String,
|
||||
status: String,
|
||||
workspace_root: String,
|
||||
workspace_name: String,
|
||||
cwd: Option<String>,
|
||||
model: Option<String>,
|
||||
provider: Option<String>,
|
||||
created_at: u64,
|
||||
updated_at: u64,
|
||||
created_by_client_id: Option<String>,
|
||||
prompt: Option<String>,
|
||||
input_tokens: Option<u64>,
|
||||
output_tokens: Option<u64>,
|
||||
total_cost: Option<f64>,
|
||||
agent_count: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
struct HubEventRecord {
|
||||
id: String,
|
||||
title: String,
|
||||
body: String,
|
||||
severity: String,
|
||||
timestamp: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize)]
|
||||
struct NotificationRecord {
|
||||
title: String,
|
||||
@@ -53,6 +86,8 @@ struct SidecarMessage {
|
||||
clients: Option<Vec<serde_json::Value>>,
|
||||
sessions: Option<Vec<serde_json::Value>>,
|
||||
client_summaries: Option<Vec<ClientSummary>>,
|
||||
session_summaries: Option<Vec<SessionSummary>>,
|
||||
events: Option<Vec<HubEventRecord>>,
|
||||
last_workspace_root: Option<String>,
|
||||
hub_uptime: Option<String>,
|
||||
title: Option<String>,
|
||||
@@ -133,6 +168,13 @@ fn push_notification(
|
||||
|
||||
fn resolve_sidecar_script(workspace_root: &str, launch_cwd: &str) -> Option<PathBuf> {
|
||||
let candidates = [
|
||||
PathBuf::from(workspace_root)
|
||||
.join("sdk")
|
||||
.join("apps")
|
||||
.join("examples")
|
||||
.join("menubar")
|
||||
.join("sidecar")
|
||||
.join("index.ts"),
|
||||
PathBuf::from(workspace_root)
|
||||
.join("apps")
|
||||
.join("examples")
|
||||
@@ -161,6 +203,16 @@ fn resolve_sidecar_binary(workspace_root: &str) -> Option<PathBuf> {
|
||||
let binary_name = sidecar_binary_name();
|
||||
let current_exe = std::env::current_exe().ok();
|
||||
let candidates = [
|
||||
Some(
|
||||
PathBuf::from(workspace_root)
|
||||
.join("sdk")
|
||||
.join("apps")
|
||||
.join("examples")
|
||||
.join("menubar")
|
||||
.join("src-tauri")
|
||||
.join("bin")
|
||||
.join(&binary_name),
|
||||
),
|
||||
Some(
|
||||
PathBuf::from(workspace_root)
|
||||
.join("apps")
|
||||
@@ -305,6 +357,8 @@ fn handle_sidecar_message(state: &Arc<AppState>, msg: SidecarMessage, raw: &str)
|
||||
if let Ok(mut hub) = state.hub_state.lock() {
|
||||
hub.connected = connected;
|
||||
hub.client_summaries = msg.client_summaries.unwrap_or_default();
|
||||
hub.session_summaries = msg.session_summaries.unwrap_or_default();
|
||||
hub.events = msg.events.unwrap_or_default();
|
||||
hub.last_workspace_root = msg
|
||||
.last_workspace_root
|
||||
.and_then(|value| if value.trim().is_empty() { None } else { Some(value) });
|
||||
@@ -517,6 +571,9 @@ fn build_tray_menu(
|
||||
hub_state.last_workspace_root.is_some(),
|
||||
None::<&str>,
|
||||
)?;
|
||||
let open_dashboard_item =
|
||||
MenuItem::with_id(app, "open_dashboard", "Open Dashboard", true, None::<&str>)?;
|
||||
items.push(Box::new(open_dashboard_item));
|
||||
items.push(Box::new(new_chat_item));
|
||||
items.push(Box::new(PredefinedMenuItem::separator(app)?));
|
||||
|
||||
@@ -554,6 +611,8 @@ fn get_hub_state(state: tauri::State<'_, Arc<AppState>>) -> serde_json::Value {
|
||||
serde_json::json!({
|
||||
"connected": hub.connected,
|
||||
"clientSummaries": hub.client_summaries,
|
||||
"sessionSummaries": hub.session_summaries,
|
||||
"events": hub.events,
|
||||
"lastWorkspaceRoot": hub.last_workspace_root,
|
||||
"hubUptime": hub.hub_uptime,
|
||||
"lastError": hub.last_error,
|
||||
@@ -562,6 +621,39 @@ fn get_hub_state(state: tauri::State<'_, Arc<AppState>>) -> serde_json::Value {
|
||||
})
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn start_new_session(prompt: String, state: tauri::State<'_, Arc<AppState>>) -> Result<(), String> {
|
||||
let prompt = prompt.trim().to_string();
|
||||
if prompt.is_empty() {
|
||||
return Err("Prompt cannot be empty".to_string());
|
||||
}
|
||||
send_sidecar_command(
|
||||
state.inner(),
|
||||
serde_json::json!({
|
||||
"type": "new_chat",
|
||||
"prompt": prompt,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
#[tauri::command]
|
||||
fn abort_session(
|
||||
session_id: String,
|
||||
state: tauri::State<'_, Arc<AppState>>,
|
||||
) -> Result<(), String> {
|
||||
let session_id = session_id.trim().to_string();
|
||||
if session_id.is_empty() {
|
||||
return Err("Session id cannot be empty".to_string());
|
||||
}
|
||||
send_sidecar_command(
|
||||
state.inner(),
|
||||
serde_json::json!({
|
||||
"type": "abort_session",
|
||||
"sessionId": session_id,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let app_state = Arc::new(AppState::new());
|
||||
let launch_cwd = std::env::current_dir()
|
||||
@@ -649,6 +741,12 @@ fn main() {
|
||||
thread::sleep(Duration::from_millis(300));
|
||||
app.exit(0);
|
||||
}
|
||||
"open_dashboard" => {
|
||||
if let Some(window) = app.get_webview_window("main") {
|
||||
let _ = window.show();
|
||||
let _ = window.set_focus();
|
||||
}
|
||||
}
|
||||
"latest_error" => {
|
||||
let state = app.state::<Arc<AppState>>();
|
||||
let error = {
|
||||
@@ -709,7 +807,11 @@ fn main() {
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![get_hub_state])
|
||||
.invoke_handler(tauri::generate_handler![
|
||||
get_hub_state,
|
||||
start_new_session,
|
||||
abort_session
|
||||
])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error running menubar app");
|
||||
}
|
||||
|
||||
@@ -6,10 +6,23 @@
|
||||
"build": {
|
||||
"beforeDevCommand": "bun run build:sidecar:bin",
|
||||
"beforeBuildCommand": "bun run build:sidecar:bin",
|
||||
"frontendDist": "../ui",
|
||||
"beforeBundleCommand": "[ \"$(uname)\" = \"Darwin\" ] && [ -e ./target/release/bundle/macos/Cline\\ Hub.app ] && xattr -d com.apple.quarantine ./target/release/bundle/macos/Cline\\ Hub.app || true"
|
||||
},
|
||||
"app": {
|
||||
"windows": [],
|
||||
"withGlobalTauri": true,
|
||||
"windows": [
|
||||
{
|
||||
"label": "main",
|
||||
"title": "Cline Hub Monitor",
|
||||
"width": 1600,
|
||||
"height": 900,
|
||||
"minWidth": 1180,
|
||||
"minHeight": 720,
|
||||
"resizable": true,
|
||||
"decorations": false
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user