generated from nhcarrigan/template
feat: add visual todo list panel
- Add TodoPanel component to display TodoWrite tool calls - Create todos Svelte store to track todo state - Emit todo-update Tauri event when TodoWrite is called - Add todo button to status bar (next to session history) - Display todos with status icons, progress bar, and completion count - Real-time updates as I work through tasks Closes #132
This commit is contained in:
@@ -64,6 +64,7 @@
|
|||||||
"@tauri-apps/plugin-store": "^2",
|
"@tauri-apps/plugin-store": "^2",
|
||||||
"codemirror": "^6.0.2",
|
"codemirror": "^6.0.2",
|
||||||
"highlight.js": "^11.11.1",
|
"highlight.js": "^11.11.1",
|
||||||
|
"lucide-svelte": "^0.563.0",
|
||||||
"marked": "^17.0.1"
|
"marked": "^17.0.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
Generated
+12
@@ -119,6 +119,9 @@ importers:
|
|||||||
highlight.js:
|
highlight.js:
|
||||||
specifier: ^11.11.1
|
specifier: ^11.11.1
|
||||||
version: 11.11.1
|
version: 11.11.1
|
||||||
|
lucide-svelte:
|
||||||
|
specifier: ^0.563.0
|
||||||
|
version: 0.563.0(svelte@5.46.3)
|
||||||
marked:
|
marked:
|
||||||
specifier: ^17.0.1
|
specifier: ^17.0.1
|
||||||
version: 17.0.1
|
version: 17.0.1
|
||||||
@@ -1668,6 +1671,11 @@ packages:
|
|||||||
resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==}
|
resolution: {integrity: sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==}
|
||||||
engines: {node: 20 || >=22}
|
engines: {node: 20 || >=22}
|
||||||
|
|
||||||
|
lucide-svelte@0.563.0:
|
||||||
|
resolution: {integrity: sha512-pjZKw7TpQcamfQrx7YdbOHgmrcNeKiGGMD0tKZQaVktwSsbqw28CsKc2Q97ttwjytiCWkJyOa8ij2Q+Og0nPfQ==}
|
||||||
|
peerDependencies:
|
||||||
|
svelte: ^3 || ^4 || ^5.0.0-next.42
|
||||||
|
|
||||||
lz-string@1.5.0:
|
lz-string@1.5.0:
|
||||||
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
|
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -3650,6 +3658,10 @@ snapshots:
|
|||||||
|
|
||||||
lru-cache@11.2.4: {}
|
lru-cache@11.2.4: {}
|
||||||
|
|
||||||
|
lucide-svelte@0.563.0(svelte@5.46.3):
|
||||||
|
dependencies:
|
||||||
|
svelte: 5.46.3
|
||||||
|
|
||||||
lz-string@1.5.0: {}
|
lz-string@1.5.0: {}
|
||||||
|
|
||||||
magic-string@0.30.21:
|
magic-string@0.30.21:
|
||||||
|
|||||||
@@ -282,6 +282,21 @@ pub struct AgentEndEvent {
|
|||||||
pub num_turns: Option<u32>,
|
pub num_turns: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TodoItem {
|
||||||
|
pub content: String,
|
||||||
|
pub status: String, // "pending", "in_progress", or "completed"
|
||||||
|
#[serde(rename = "activeForm")]
|
||||||
|
pub active_form: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct TodoUpdateEvent {
|
||||||
|
pub todos: Vec<TodoItem>,
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub conversation_id: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ use crate::stats::{calculate_cost, StatsUpdateEvent, UsageStats};
|
|||||||
use crate::types::{
|
use crate::types::{
|
||||||
AgentEndEvent, AgentStartEvent, CharacterState, ClaudeMessage, ConnectionEvent,
|
AgentEndEvent, AgentStartEvent, CharacterState, ClaudeMessage, ConnectionEvent,
|
||||||
ConnectionStatus, ContentBlock, MessageCost, OutputEvent, PermissionPromptEvent,
|
ConnectionStatus, ContentBlock, MessageCost, OutputEvent, PermissionPromptEvent,
|
||||||
PermissionPromptEventItem, QuestionOption, SessionEvent, StateChangeEvent,
|
PermissionPromptEventItem, QuestionOption, SessionEvent, StateChangeEvent, TodoItem,
|
||||||
UserQuestionEvent, WorkingDirectoryEvent,
|
TodoUpdateEvent, UserQuestionEvent, WorkingDirectoryEvent,
|
||||||
};
|
};
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
@@ -937,6 +937,34 @@ fn process_json_line(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Emit todo-update event for TodoWrite tool invocations
|
||||||
|
if name == "TodoWrite" {
|
||||||
|
if let Some(todos_value) = input.get("todos") {
|
||||||
|
if let Some(todos_array) = todos_value.as_array() {
|
||||||
|
let todos: Vec<TodoItem> = todos_array
|
||||||
|
.iter()
|
||||||
|
.filter_map(|todo| {
|
||||||
|
serde_json::from_value(todo.clone()).ok()
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
tracing::debug!(
|
||||||
|
"Emitting todo-update: {} todos, parent={:?}",
|
||||||
|
todos.len(),
|
||||||
|
parent_tool_use_id
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = app.emit(
|
||||||
|
"claude:todo-update",
|
||||||
|
TodoUpdateEvent {
|
||||||
|
todos,
|
||||||
|
conversation_id: conversation_id.clone(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let desc = format_tool_description(name, input);
|
let desc = format_tool_description(name, input);
|
||||||
let _ = app.emit(
|
let _ = app.emit(
|
||||||
"claude:output",
|
"claude:output",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@
|
|||||||
import { achievementProgress } from "$lib/stores/achievements";
|
import { achievementProgress } from "$lib/stores/achievements";
|
||||||
import { runningAgentCount } from "$lib/stores/agents";
|
import { runningAgentCount } from "$lib/stores/agents";
|
||||||
import SessionHistoryPanel from "./SessionHistoryPanel.svelte";
|
import SessionHistoryPanel from "./SessionHistoryPanel.svelte";
|
||||||
|
import TodoPanel from "./TodoPanel.svelte";
|
||||||
import GitPanel from "./GitPanel.svelte";
|
import GitPanel from "./GitPanel.svelte";
|
||||||
import ProfilePanel from "./ProfilePanel.svelte";
|
import ProfilePanel from "./ProfilePanel.svelte";
|
||||||
import AgentMonitorPanel from "./AgentMonitorPanel.svelte";
|
import AgentMonitorPanel from "./AgentMonitorPanel.svelte";
|
||||||
@@ -49,6 +50,7 @@
|
|||||||
let showHelp = $state(false);
|
let showHelp = $state(false);
|
||||||
let showKeyboardShortcuts = $state(false);
|
let showKeyboardShortcuts = $state(false);
|
||||||
let showSessionHistory = $state(false);
|
let showSessionHistory = $state(false);
|
||||||
|
let showTodoPanel = $state(false);
|
||||||
let showGitPanel = $state(false);
|
let showGitPanel = $state(false);
|
||||||
let showProfile = $state(false);
|
let showProfile = $state(false);
|
||||||
let showAgentMonitor = $state(false);
|
let showAgentMonitor = $state(false);
|
||||||
@@ -438,6 +440,20 @@
|
|||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
|
<button
|
||||||
|
onclick={() => (showTodoPanel = true)}
|
||||||
|
class="p-1 text-gray-500 icon-trans-hover"
|
||||||
|
title="Todo List"
|
||||||
|
>
|
||||||
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onclick={() => (showGitPanel = true)}
|
onclick={() => (showGitPanel = true)}
|
||||||
class="p-1 text-gray-500 icon-trans-hover"
|
class="p-1 text-gray-500 icon-trans-hover"
|
||||||
@@ -673,6 +689,10 @@
|
|||||||
<SessionHistoryPanel onClose={() => (showSessionHistory = false)} />
|
<SessionHistoryPanel onClose={() => (showSessionHistory = false)} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
{#if showTodoPanel}
|
||||||
|
<TodoPanel onClose={() => (showTodoPanel = false)} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if showGitPanel}
|
{#if showGitPanel}
|
||||||
<GitPanel isOpen={showGitPanel} onClose={() => (showGitPanel = false)} />
|
<GitPanel isOpen={showGitPanel} onClose={() => (showGitPanel = false)} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -0,0 +1,182 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { todos, type TodoItem } from "$lib/stores/todos";
|
||||||
|
import { CheckCircle, Circle, Loader } from "lucide-svelte";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
onClose: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { onClose }: Props = $props();
|
||||||
|
|
||||||
|
const currentTodos = $derived($todos);
|
||||||
|
const hasTodos = $derived(currentTodos.length > 0);
|
||||||
|
const completedCount = $derived(currentTodos.filter((t) => t.status === "completed").length);
|
||||||
|
const totalCount = $derived(currentTodos.length);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="fixed top-0 right-0 h-full w-96 bg-gray-900 border-l border-pink-500/30 shadow-2xl flex flex-col z-50"
|
||||||
|
>
|
||||||
|
<!-- Header -->
|
||||||
|
<div class="flex items-center justify-between p-4 border-b border-pink-500/30">
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="text-pink-400">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-6 w-6"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<h2 class="text-lg font-semibold text-white">Hikari's Todo List</h2>
|
||||||
|
{#if hasTodos}
|
||||||
|
<p class="text-xs text-gray-400">
|
||||||
|
{completedCount} of {totalCount} completed
|
||||||
|
</p>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onclick={onClose}
|
||||||
|
class="text-gray-400 hover:text-white transition-colors p-1 rounded-lg hover:bg-gray-800"
|
||||||
|
aria-label="Close todo panel"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-5 w-5"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
fill-rule="evenodd"
|
||||||
|
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="flex-1 overflow-y-auto p-4">
|
||||||
|
{#if !hasTodos}
|
||||||
|
<div class="flex flex-col items-center justify-center h-full text-gray-500">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="h-16 w-16 mb-4 opacity-50"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke="currentColor"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
stroke-width="2"
|
||||||
|
d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<p class="text-center">No active todos</p>
|
||||||
|
<p class="text-sm text-center mt-2">I'll update this when I start working on tasks!</p>
|
||||||
|
</div>
|
||||||
|
{:else}
|
||||||
|
<div class="space-y-2">
|
||||||
|
{#each currentTodos as todo (todo.content)}
|
||||||
|
<div
|
||||||
|
class="group bg-gray-800/50 rounded-lg p-3 border border-gray-700 hover:border-pink-500/50 transition-all"
|
||||||
|
class:opacity-60={todo.status === "completed"}
|
||||||
|
>
|
||||||
|
<div class="flex items-start gap-3">
|
||||||
|
<!-- Status Icon -->
|
||||||
|
<div class="mt-0.5 flex-shrink-0">
|
||||||
|
{#if todo.status === "completed"}
|
||||||
|
<CheckCircle class="w-5 h-5 text-green-400" />
|
||||||
|
{:else if todo.status === "in_progress"}
|
||||||
|
<Loader class="w-5 h-5 text-pink-400 animate-spin" />
|
||||||
|
{:else}
|
||||||
|
<Circle class="w-5 h-5 text-gray-500" />
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div class="flex-1 min-w-0">
|
||||||
|
<p
|
||||||
|
class="text-sm font-medium"
|
||||||
|
class:text-gray-400={todo.status === "completed"}
|
||||||
|
class:line-through={todo.status === "completed"}
|
||||||
|
class:text-white={todo.status !== "completed"}
|
||||||
|
>
|
||||||
|
{todo.status === "in_progress" ? todo.activeForm : todo.content}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Status Badge -->
|
||||||
|
<div class="mt-1">
|
||||||
|
{#if todo.status === "completed"}
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-900/30 text-green-400 border border-green-500/30"
|
||||||
|
>
|
||||||
|
✓ Completed
|
||||||
|
</span>
|
||||||
|
{:else if todo.status === "in_progress"}
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-pink-900/30 text-pink-400 border border-pink-500/30 animate-pulse"
|
||||||
|
>
|
||||||
|
⚡ In Progress
|
||||||
|
</span>
|
||||||
|
{:else}
|
||||||
|
<span
|
||||||
|
class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-700/30 text-gray-400 border border-gray-600/30"
|
||||||
|
>
|
||||||
|
○ Pending
|
||||||
|
</span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Footer with Progress Bar -->
|
||||||
|
{#if hasTodos}
|
||||||
|
<div class="border-t border-pink-500/30 p-4 bg-gray-800/50">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<span class="text-xs font-medium text-gray-400">Progress</span>
|
||||||
|
<span class="text-xs font-medium text-pink-400">
|
||||||
|
{Math.round((completedCount / totalCount) * 100)}%
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="w-full bg-gray-700 rounded-full h-2 overflow-hidden">
|
||||||
|
<div
|
||||||
|
class="bg-gradient-to-r from-pink-500 to-purple-500 h-2 rounded-full transition-all duration-500 ease-out"
|
||||||
|
style="width: {(completedCount / totalCount) * 100}%"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@keyframes pulse {
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.animate-pulse {
|
||||||
|
animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { writable } from "svelte/store";
|
||||||
|
import { listen } from "@tauri-apps/api/event";
|
||||||
|
|
||||||
|
export interface TodoItem {
|
||||||
|
content: string;
|
||||||
|
status: "pending" | "in_progress" | "completed";
|
||||||
|
activeForm: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TodoUpdatePayload {
|
||||||
|
todos: TodoItem[];
|
||||||
|
conversation_id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the writable store
|
||||||
|
const { subscribe, set, update } = writable<TodoItem[]>([]);
|
||||||
|
|
||||||
|
// Listen for todo updates from the backend
|
||||||
|
let unlisten: (() => void) | undefined;
|
||||||
|
|
||||||
|
export async function initializeTodoListener(): Promise<void> {
|
||||||
|
if (unlisten) {
|
||||||
|
return; // Already initialized
|
||||||
|
}
|
||||||
|
|
||||||
|
unlisten = await listen<TodoUpdatePayload>("claude:todo-update", (event) => {
|
||||||
|
set(event.payload.todos);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cleanupTodoListener(): void {
|
||||||
|
if (unlisten) {
|
||||||
|
unlisten();
|
||||||
|
unlisten = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Export the store
|
||||||
|
export const todos = {
|
||||||
|
subscribe,
|
||||||
|
set,
|
||||||
|
update,
|
||||||
|
clear: () => set([]),
|
||||||
|
};
|
||||||
@@ -35,6 +35,7 @@
|
|||||||
import CloseAppConfirmModal from "$lib/components/CloseAppConfirmModal.svelte";
|
import CloseAppConfirmModal from "$lib/components/CloseAppConfirmModal.svelte";
|
||||||
import MemoryBrowserPanel from "$lib/components/MemoryBrowserPanel.svelte";
|
import MemoryBrowserPanel from "$lib/components/MemoryBrowserPanel.svelte";
|
||||||
import { debugConsoleStore } from "$lib/stores/debugConsole";
|
import { debugConsoleStore } from "$lib/stores/debugConsole";
|
||||||
|
import { initializeTodoListener, cleanupTodoListener } from "$lib/stores/todos";
|
||||||
|
|
||||||
let initialized = false;
|
let initialized = false;
|
||||||
let updateNotification: UpdateNotification | undefined = $state(undefined);
|
let updateNotification: UpdateNotification | undefined = $state(undefined);
|
||||||
@@ -445,6 +446,9 @@
|
|||||||
// Initialize Discord RPC
|
// Initialize Discord RPC
|
||||||
await initializeDiscordRpc();
|
await initializeDiscordRpc();
|
||||||
|
|
||||||
|
// Initialize todo listener
|
||||||
|
await initializeTodoListener();
|
||||||
|
|
||||||
// Listen for window close requests
|
// Listen for window close requests
|
||||||
const unlisten = await listen("window-close-requested", () => {
|
const unlisten = await listen("window-close-requested", () => {
|
||||||
handleCloseRequest();
|
handleCloseRequest();
|
||||||
@@ -461,6 +465,7 @@
|
|||||||
if (initialized) {
|
if (initialized) {
|
||||||
cleanupTauriListeners();
|
cleanupTauriListeners();
|
||||||
cleanupNotificationSync();
|
cleanupNotificationSync();
|
||||||
|
cleanupTodoListener();
|
||||||
stopDiscordRpc();
|
stopDiscordRpc();
|
||||||
window.removeEventListener("keydown", handleGlobalKeydown);
|
window.removeEventListener("keydown", handleGlobalKeydown);
|
||||||
initialized = false;
|
initialized = false;
|
||||||
|
|||||||
Reference in New Issue
Block a user