diff --git a/package-lock.json b/package-lock.json
index b868807..f8e797b 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "afkode",
- "version": "0.8.12",
+ "version": "0.8.14",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "afkode",
- "version": "0.8.12",
+ "version": "0.8.14",
"dependencies": {
"@tauri-apps/api": "^2.11.1",
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
diff --git a/package.json b/package.json
index 2696fe5..8a7ae50 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"name": "afkode",
"private": true,
- "version": "0.8.13",
+ "version": "0.8.14",
"type": "module",
"scripts": {
"dev": "vite",
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index feb0f53..a321a40 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -10,7 +10,7 @@ checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "afkode"
-version = "0.8.13"
+version = "0.8.14"
dependencies = [
"base64 0.22.1",
"core-foundation",
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 707ff0c..d743253 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "afkode"
-version = "0.8.13"
+version = "0.8.14"
description = "In-game overlay to supervise AI coding agents while you play"
authors = ["Omar Hernandez"]
license = "MIT"
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index 5b9888f..39f7fea 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "AFKode",
- "version": "0.8.13",
+ "version": "0.8.14",
"identifier": "app.afkode.overlay",
"build": {
"beforeDevCommand": "npm run dev",
diff --git a/src/main.ts b/src/main.ts
index efc8905..d409759 100644
--- a/src/main.ts
+++ b/src/main.ts
@@ -26,6 +26,7 @@ import { CommandBlocks } from "./blocks";
import { WebKitDeadKeyAddon } from "./xtermDeadKeyAddon";
import { MODEL_EXTS } from "./modelExts";
import type { ModelPreview } from "./modelPreview";
+import { startTour, type TourStep } from "./tour";
// ── Themes ────────────────────────────────────────────────
@@ -357,6 +358,20 @@ const I18N: Record
> = {
restoreAsk: "Preguntar",
restoreNever: "Nunca",
restoreNote: "Reabre tus pestañas (orden, nombres, colores y carpetas). Las pestañas de Claude ofrecen retomar la conversación anterior.",
+ tourLabel: "Recorrido guiado",
+ tourReplayBtn: "Ver el recorrido",
+ tourSkip: "Saltar",
+ tourNext: "Siguiente",
+ tourDone: "Listo",
+ tourStepOf: "Paso {i} de {n}",
+ tourStep1Title: "Abre una nueva pestaña",
+ tourStep1Body: "El botón + lanza Claude Code, OpenCode, Codex o PowerShell en la carpeta que elijas.",
+ tourStep2Title: "Tu terminal",
+ tourStep2Body: "ConPTY con render por GPU: colores completos y apps interactivas con impacto mínimo en tus FPS.",
+ tourStep3Title: "Vista previa de archivos",
+ tourStep3Body: "Al abrir un archivo se muestra aquí: código, imágenes e incluso modelos 3D (.glb, .gltf, .obj, .stl).",
+ tourStep4Title: "Búsqueda global",
+ tourStep4Body: "Pulsa Ctrl+K en cualquier momento para saltar a archivos, comandos o sesiones.",
restoreBannerText: "¿Restaurar tu última sesión? ({n} pestañas)",
restoreBtn: "Restaurar",
resumeQuestion: "¿Retomar la conversación anterior?",
@@ -484,6 +499,20 @@ const I18N: Record> = {
restoreAsk: "Ask",
restoreNever: "Never",
restoreNote: "Reopens your tabs (order, names, colors and folders). Claude tabs offer to resume the previous conversation.",
+ tourLabel: "Guided tour",
+ tourReplayBtn: "Take the tour",
+ tourSkip: "Skip",
+ tourNext: "Next",
+ tourDone: "Done",
+ tourStepOf: "Step {i} of {n}",
+ tourStep1Title: "Open a new tab",
+ tourStep1Body: "The + button launches Claude Code, OpenCode, Codex, or PowerShell in a folder you pick.",
+ tourStep2Title: "Your terminal",
+ tourStep2Body: "ConPTY with GPU rendering: full colors and interactive apps with minimal impact on your FPS.",
+ tourStep3Title: "File preview",
+ tourStep3Body: "Open a file and it shows up here: code, images, and even 3D models (.glb, .gltf, .obj, .stl).",
+ tourStep4Title: "Global search",
+ tourStep4Body: "Press Ctrl+K anytime to jump to files, commands, or sessions.",
restoreBannerText: "Restore your last session? ({n} tabs)",
restoreBtn: "Restore",
resumeQuestion: "Resume previous conversation?",
@@ -611,6 +640,20 @@ const I18N: Record> = {
restoreAsk: "Demander",
restoreNever: "Jamais",
restoreNote: "Rouvre vos onglets (ordre, noms, couleurs et dossiers). Les onglets Claude proposent de reprendre la conversation précédente.",
+ tourLabel: "Visite guidée",
+ tourReplayBtn: "Revoir la visite",
+ tourSkip: "Passer",
+ tourNext: "Suivant",
+ tourDone: "Terminé",
+ tourStepOf: "Étape {i} sur {n}",
+ tourStep1Title: "Ouvrez un nouvel onglet",
+ tourStep1Body: "Le bouton + lance Claude Code, OpenCode, Codex ou PowerShell dans le dossier de votre choix.",
+ tourStep2Title: "Votre terminal",
+ tourStep2Body: "ConPTY avec rendu GPU : couleurs complètes et applications interactives, impact minimal sur vos FPS.",
+ tourStep3Title: "Aperçu des fichiers",
+ tourStep3Body: "Ouvrez un fichier et il s'affiche ici : code, images et même modèles 3D (.glb, .gltf, .obj, .stl).",
+ tourStep4Title: "Recherche globale",
+ tourStep4Body: "Appuyez sur Ctrl+K à tout moment pour accéder aux fichiers, commandes ou sessions.",
restoreBannerText: "Restaurer votre dernière session ? ({n} onglets)",
restoreBtn: "Restaurer",
resumeQuestion: "Reprendre la conversation précédente ?",
@@ -738,6 +781,20 @@ const I18N: Record> = {
restoreAsk: "Chiedi",
restoreNever: "Mai",
restoreNote: "Riapre le tue schede (ordine, nomi, colori e cartelle). Le schede Claude offrono di riprendere la conversazione precedente.",
+ tourLabel: "Visita guidata",
+ tourReplayBtn: "Rivedi la visita",
+ tourSkip: "Salta",
+ tourNext: "Avanti",
+ tourDone: "Fine",
+ tourStepOf: "Passo {i} di {n}",
+ tourStep1Title: "Apri una nuova scheda",
+ tourStep1Body: "Il pulsante + avvia Claude Code, OpenCode, Codex o PowerShell nella cartella che scegli.",
+ tourStep2Title: "Il tuo terminale",
+ tourStep2Body: "ConPTY con rendering GPU: colori completi e app interattive con impatto minimo sugli FPS.",
+ tourStep3Title: "Anteprima file",
+ tourStep3Body: "Apri un file e viene mostrato qui: codice, immagini e persino modelli 3D (.glb, .gltf, .obj, .stl).",
+ tourStep4Title: "Ricerca globale",
+ tourStep4Body: "Premi Ctrl+K in qualsiasi momento per saltare a file, comandi o sessioni.",
restoreBannerText: "Ripristinare l'ultima sessione? ({n} schede)",
restoreBtn: "Ripristina",
resumeQuestion: "Riprendere la conversazione precedente?",
@@ -2597,6 +2654,55 @@ wireSwitch("#chk-hooks", "hooks");
wireSwitch("#chk-matchmode", "matchMode");
wireSwitch("#chk-aisearch", "aiSearch");
+// ── Onboarding tour ───────────────────────────────────────
+
+function buildTourSteps(): TourStep[] {
+ const previewModal = $("#file-preview-modal");
+ const wasOpen = previewModal.classList.contains("open");
+ return [
+ {
+ selector: "#btn-new-tab",
+ title: t("tourStep1Title"),
+ body: t("tourStep1Body"),
+ },
+ {
+ selector: "#terminals",
+ title: t("tourStep2Title"),
+ body: t("tourStep2Body"),
+ },
+ {
+ selector: "#file-preview-modal",
+ title: t("tourStep3Title"),
+ body: t("tourStep3Body"),
+ onEnter: () => previewModal.classList.add("open"),
+ onLeave: () => {
+ if (!wasOpen) previewModal.classList.remove("open");
+ },
+ },
+ {
+ selector: "#btn-global-search",
+ title: t("tourStep4Title"),
+ body: t("tourStep4Body"),
+ },
+ ];
+}
+
+function tourLabels() {
+ return {
+ skip: t("tourSkip"),
+ next: t("tourNext"),
+ done: t("tourDone"),
+ stepOf: (i: number, n: number) => t("tourStepOf").replace("{i}", String(i)).replace("{n}", String(n)),
+ };
+}
+
+function replayTour() {
+ $("#settings-modal").classList.add("hidden");
+ startTour(buildTourSteps(), tourLabels(), () => {});
+}
+
+$("#btn-replay-tour").addEventListener("click", replayTour);
+
const selRestore = $("#sel-restore");
selRestore.value = settings.restore;
selRestore.addEventListener("change", () => {
@@ -3594,8 +3700,10 @@ refreshCliButtons().then(() => {
}
});
-// First run: show the help card once so the user discovers the features.
-if (!localStorage.getItem("help-seen")) {
- helpModal.classList.remove("hidden");
- localStorage.setItem("help-seen", "1");
+// First run: walk the user through the core UI once. The static help
+// card (still reachable any time via the help button) stays available
+// as reference material afterward.
+if (!localStorage.getItem("tour-seen")) {
+ localStorage.setItem("tour-seen", "1");
+ startTour(buildTourSteps(), tourLabels(), () => {});
}
diff --git a/src/styles.css b/src/styles.css
index 7b746ac..1b6eb05 100644
--- a/src/styles.css
+++ b/src/styles.css
@@ -1685,3 +1685,84 @@ kbd {
.hidden {
display: none !important;
}
+
+/* ── Onboarding tour ───────────────────────────────────── */
+
+.btn-replay-tour {
+ padding: 6px 12px;
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ border-radius: 7px;
+ background: rgba(255, 255, 255, 0.05);
+ color: var(--text);
+ font-size: 12px;
+ cursor: pointer;
+}
+.btn-replay-tour:hover {
+ border-color: var(--accent);
+ color: var(--accent);
+}
+
+.tour-overlay {
+ position: fixed;
+ inset: 0;
+ z-index: 300;
+ animation: fade-in 0.15s ease;
+}
+
+.tour-spot {
+ position: fixed;
+ border-radius: 8px;
+ border: 2px solid var(--accent);
+ box-shadow:
+ 0 0 0 9999px rgba(10, 11, 15, 0.72),
+ 0 0 12px 1px var(--accent-glow);
+ transition:
+ left 0.25s ease,
+ top 0.25s ease,
+ width 0.25s ease,
+ height 0.25s ease;
+ pointer-events: none;
+}
+
+.tour-tip {
+ position: fixed;
+ width: min(320px, 90vw);
+ padding: 14px 16px 12px;
+ background: rgb(var(--bar-rgb));
+ border: 1px solid var(--border);
+ border-radius: var(--radius);
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.45);
+ transition:
+ left 0.25s ease,
+ top 0.25s ease;
+}
+
+.tour-tip-step {
+ font-size: 11px;
+ color: var(--text-dim);
+ margin-bottom: 2px;
+}
+
+.tour-tip-title {
+ margin: 0 0 6px;
+ font-size: 14px;
+ color: var(--text-bright);
+}
+
+.tour-tip-body {
+ margin: 0 0 12px;
+ font-size: 12.5px;
+ line-height: 1.5;
+ color: var(--text);
+}
+
+.tour-tip-actions {
+ display: flex;
+ justify-content: flex-end;
+ gap: 8px;
+}
+
+.tour-tip-actions .ask-btn.ok {
+ border-color: var(--accent);
+ color: var(--accent);
+}
diff --git a/src/tour.ts b/src/tour.ts
new file mode 100644
index 0000000..fdc59d4
--- /dev/null
+++ b/src/tour.ts
@@ -0,0 +1,137 @@
+// First-run onboarding tour: a spotlight overlay that walks through a
+// fixed list of real UI elements. Kept framework-free and DOM-driven to
+// match the rest of the app.
+
+export interface TourStep {
+ selector: string;
+ title: string;
+ body: string;
+ onEnter?: () => void;
+ onLeave?: () => void;
+}
+
+export interface TourLabels {
+ skip: string;
+ next: string;
+ done: string;
+ stepOf: (i: number, n: number) => string;
+}
+
+let active = false;
+
+export function isTourActive(): boolean {
+ return active;
+}
+
+export function startTour(
+ steps: TourStep[],
+ labels: TourLabels,
+ onEnd: (skipped: boolean) => void,
+): void {
+ if (active || !steps.length) return;
+ active = true;
+
+ const overlay = document.createElement("div");
+ overlay.className = "tour-overlay";
+ overlay.innerHTML = `
+
+
+ `;
+ document.body.appendChild(overlay);
+
+ const spot = overlay.querySelector(".tour-spot")!;
+ const tip = overlay.querySelector(".tour-tip")!;
+ const stepEl = overlay.querySelector(".tour-tip-step")!;
+ const titleEl = overlay.querySelector(".tour-tip-title")!;
+ const bodyEl = overlay.querySelector(".tour-tip-body")!;
+ const skipBtn = overlay.querySelector(".tour-skip")!;
+ const nextBtn = overlay.querySelector(".tour-next")!;
+
+ skipBtn.textContent = labels.skip;
+
+ let index = 0;
+
+ function place() {
+ const step = steps[index];
+ const target = document.querySelector(step.selector);
+ if (!target) {
+ advance();
+ return;
+ }
+ const r = target.getBoundingClientRect();
+ const pad = 6;
+ spot.style.left = `${r.left - pad}px`;
+ spot.style.top = `${r.top - pad}px`;
+ spot.style.width = `${r.width + pad * 2}px`;
+ spot.style.height = `${r.height + pad * 2}px`;
+
+ stepEl.textContent = labels.stepOf(index + 1, steps.length);
+ titleEl.textContent = step.title;
+ bodyEl.textContent = step.body;
+ nextBtn.textContent = index === steps.length - 1 ? labels.done : labels.next;
+
+ // Position the tip below the target, flipping above if it wouldn't
+ // fit, and clamping horizontally so it never spills off-screen.
+ const tipRect = tip.getBoundingClientRect();
+ const margin = 12;
+ let top = r.bottom + margin;
+ if (top + tipRect.height > window.innerHeight) {
+ top = Math.max(margin, r.top - tipRect.height - margin);
+ }
+ let left = r.left;
+ left = Math.min(left, window.innerWidth - tipRect.width - margin);
+ left = Math.max(margin, left);
+ tip.style.top = `${top}px`;
+ tip.style.left = `${left}px`;
+ }
+
+ function enterStep() {
+ steps[index].onEnter?.();
+ // Let onEnter's DOM changes (e.g. opening a panel) settle before
+ // measuring positions off it.
+ requestAnimationFrame(place);
+ }
+
+ function leaveStep() {
+ steps[index].onLeave?.();
+ }
+
+ function advance() {
+ leaveStep();
+ index++;
+ if (index >= steps.length) {
+ finish(false);
+ return;
+ }
+ enterStep();
+ }
+
+ function finish(skipped: boolean) {
+ if (!active) return;
+ if (index < steps.length) leaveStep();
+ active = false;
+ window.removeEventListener("resize", place);
+ document.removeEventListener("keydown", onKeydown);
+ overlay.remove();
+ onEnd(skipped);
+ }
+
+ function onKeydown(e: KeyboardEvent) {
+ if (e.key === "Escape") finish(true);
+ }
+
+ nextBtn.addEventListener("click", advance);
+ skipBtn.addEventListener("click", () => finish(true));
+ window.addEventListener("resize", place);
+ document.addEventListener("keydown", onKeydown);
+
+ enterStep();
+}