neoagent 1.4.1 → 1.4.4

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.
@@ -5,6 +5,24 @@ if (window.mermaid) {
5
5
  mermaid.initialize({ startOnLoad: false, theme: "dark" });
6
6
  }
7
7
 
8
+ // ── Theme (follows OS preference automatically) ──
9
+
10
+ function applyTheme(isDark) {
11
+ document.documentElement.dataset.theme = isDark ? "dark" : "light";
12
+ if (window.mermaid) {
13
+ mermaid.initialize({ startOnLoad: false, theme: isDark ? "dark" : "default" });
14
+ }
15
+ if (window.pixelWorld) {
16
+ window.pixelWorld.syncTheme();
17
+ }
18
+ }
19
+
20
+ const _mq = window.matchMedia("(prefers-color-scheme: dark)");
21
+ applyTheme(_mq.matches);
22
+ _mq.addEventListener("change", (e) => applyTheme(e.matches));
23
+
24
+
25
+
8
26
  // Global utility to re-run mermaid
9
27
  function renderMermaids() {
10
28
  if (window.mermaid) {
@@ -77,7 +95,32 @@ function formatTime(ts) {
77
95
 
78
96
  // ── Navigation ──
79
97
 
80
- function navigateTo(page) {
98
+ const DEFAULT_PAGE = "chat";
99
+ const VALID_PAGES = new Set([
100
+ "chat",
101
+ "world",
102
+ "messaging",
103
+ "mcp",
104
+ "scheduler",
105
+ "memory",
106
+ "skills",
107
+ "protocols",
108
+ "logs",
109
+ ]);
110
+
111
+ function getPageFromLocation() {
112
+ const match = window.location.pathname.match(/^\/app\/([^/]+)$/);
113
+ const candidate = match?.[1] || (window.location.pathname === "/app" ? DEFAULT_PAGE : null);
114
+ return VALID_PAGES.has(candidate) ? candidate : DEFAULT_PAGE;
115
+ }
116
+
117
+ function buildPageUrl(page) {
118
+ return page === DEFAULT_PAGE ? "/app" : `/app/${page}`;
119
+ }
120
+
121
+ function navigateTo(page, { push = true } = {}) {
122
+ if (!VALID_PAGES.has(page)) page = DEFAULT_PAGE;
123
+
81
124
  $$(".page").forEach((p) => p.classList.remove("active"));
82
125
  $$(".sidebar-btn").forEach((b) => b.classList.remove("active"));
83
126
 
@@ -88,22 +131,25 @@ function navigateTo(page) {
88
131
  if (btn) btn.classList.add("active");
89
132
  }
90
133
 
134
+ if (push) {
135
+ const nextUrl = buildPageUrl(page);
136
+ if (window.location.pathname !== nextUrl) {
137
+ window.history.pushState({ page }, "", nextUrl);
138
+ }
139
+ }
140
+
91
141
  if (page === "memory") loadMemoryPage();
92
142
  if (page === "skills") loadSkillsPage();
93
143
  if (page === "mcp") loadMCPPage();
94
144
  if (page === "scheduler") loadSchedulerPage();
95
145
  if (page === "messaging") loadMessagingPage();
96
146
  if (page === "protocols") loadProtocolsPage();
97
- if (page === "activity") {
147
+ if (page === "world") {
98
148
  requestAnimationFrame(() => {
99
- ensureTimeline();
100
- loadActivityHistory();
101
- if (activityTimeline && activityTimeline.stepCount === 0) {
102
- api("/agents?limit=1").then(data => {
103
- if (data.runs && data.runs.length > 0) {
104
- loadRunOnCanvas(data.runs[0].id, data.runs[0].title, data.runs[0].status);
105
- }
106
- }).catch(console.error);
149
+ ensureWorld();
150
+ if (pixelWorld) {
151
+ pixelWorld.resize();
152
+ pixelWorld.refreshSummary();
107
153
  }
108
154
  });
109
155
  }
@@ -114,6 +160,10 @@ $$(".sidebar-btn[data-page]").forEach((btn) => {
114
160
  btn.addEventListener("click", () => navigateTo(btn.dataset.page));
115
161
  });
116
162
 
163
+ window.addEventListener("popstate", () => {
164
+ navigateTo(getPageFromLocation(), { push: false });
165
+ });
166
+
117
167
  // ── Chat ──
118
168
 
119
169
  const chatInput = $("#chatInput");
@@ -155,8 +205,8 @@ function sendMessage() {
155
205
  chatMessages.appendChild(thinkingEl);
156
206
  chatMessages.scrollTop = chatMessages.scrollHeight;
157
207
 
158
- // Reset activity for new run
159
- clearActivity();
208
+ // Reset world focus for the incoming run
209
+ resetWorldForNewRun();
160
210
 
161
211
  socket.emit("agent:run", { task: text });
162
212
  }
@@ -335,7 +385,7 @@ function renderMarkdown(text) {
335
385
  return html;
336
386
  }
337
387
 
338
- // ── Activity Helpers ──
388
+ // ── World Helpers ──
339
389
 
340
390
  const TOOL_META = {
341
391
  execute_command: { icon: "⚡", label: "Terminal", color: "cli" },
@@ -521,375 +571,860 @@ function describeResult(toolName, result) {
521
571
  }
522
572
  }
523
573
 
524
- // ── Activity Timeline ──
574
+ class PixelWorld {
575
+ constructor(canvas) {
576
+ this.canvas = canvas;
577
+ this.ctx = canvas.getContext("2d");
578
+ this.buffer = document.createElement("canvas");
579
+ this.buffer.width = 384;
580
+ this.buffer.height = 216;
581
+ this.bctx = this.buffer.getContext("2d");
582
+ this.dpr = Math.max(1, window.devicePixelRatio || 1);
583
+ this.tick = 0;
584
+ this.runId = null;
585
+ this.runMode = "idle";
586
+ this.activeTool = null;
587
+ this.taskLabel = "No active run";
588
+ this.statusLabel = "Ambient systems nominal";
589
+ this.totalTools = 0;
590
+ this.totalMessages = 0;
591
+ this.helperCounter = 0;
592
+ this.scanFlash = 0;
593
+ this.socialPulse = 0;
594
+ this.errorFlash = 0;
595
+ this.recentEvents = [];
596
+ this.historyLoaded = false;
597
+ this.stepAssignments = new Map();
598
+ this.structures = [
599
+ { key: "core", x: 138, y: 78, w: 104, h: 64, color: "#22c55e", glow: 0, label: "Lead Desk" },
600
+ { key: "browser", x: 20, y: 16, w: 96, h: 80, color: "#3b82f6", glow: 0, label: "Research Corner" },
601
+ { key: "memory", x: 286, y: 18, w: 82, h: 78, color: "#f59e0b", glow: 0, label: "Archive Wall" },
602
+ { key: "cli", x: 16, y: 108, w: 108, h: 84, color: "#f97316", glow: 0, label: "Ops Bench" },
603
+ { key: "social", x: 288, y: 116, w: 80, h: 74, color: "#ec4899", glow: 0, label: "Comms Desk" },
604
+ ];
605
+ this.helperSlots = [
606
+ { x: 134, y: 160 },
607
+ { x: 250, y: 160 },
608
+ { x: 164, y: 176 },
609
+ { x: 222, y: 176 },
610
+ ];
611
+ this.mainAgent = {
612
+ id: "lead-agent",
613
+ name: "NeoAgent",
614
+ type: "lead",
615
+ x: 192,
616
+ y: 104,
617
+ tint: "#9ef7dc",
618
+ phase: 0.8,
619
+ focus: "core",
620
+ specialty: "Orchestrating the whole task",
621
+ status: "Waiting for the next task",
622
+ lastActive: 0,
623
+ };
624
+ this.helpers = [];
625
+ this.packets = [];
626
+ this.palette = null;
627
+ this.officeImages = {
628
+ dark: this.loadImage("/assets/world-office-dark.png"),
629
+ light: this.loadImage("/assets/world-office-light.png"),
630
+ };
631
+ this.ui = {
632
+ modePill: $("#worldModePill"),
633
+ toolPill: $("#worldToolPill"),
634
+ task: $("#worldTaskValue"),
635
+ status: $("#worldStatusValue"),
636
+ mode: $("#worldModeValue"),
637
+ run: $("#worldRunValue"),
638
+ tools: $("#worldToolsValue"),
639
+ helpers: $("#worldHelpersValue"),
640
+ messages: $("#worldMessagesValue"),
641
+ agents: $("#worldAgentList"),
642
+ events: $("#worldEventList"),
643
+ badge: $("#worldBadge"),
644
+ };
645
+
646
+ this.resize = this.resize.bind(this);
647
+ this.loop = this.loop.bind(this);
648
+ window.addEventListener("resize", this.resize);
649
+ this.syncTheme();
650
+ this.resize();
651
+ this.renderAgents();
652
+ this.renderEventList();
653
+ this.renderHud();
654
+ requestAnimationFrame(this.loop);
655
+ }
656
+
657
+ resize() {
658
+ const rect = this.canvas.getBoundingClientRect();
659
+ const width = Math.max(320, Math.floor(rect.width || this.canvas.clientWidth || 640));
660
+ const height = Math.max(320, Math.floor(rect.height || this.canvas.clientHeight || 560));
661
+ this.canvas.width = Math.floor(width * this.dpr);
662
+ this.canvas.height = Math.floor(height * this.dpr);
663
+ this.ctx.setTransform(1, 0, 0, 1, 0, 0);
664
+ this.ctx.imageSmoothingEnabled = false;
665
+ }
666
+
667
+ syncTheme() {
668
+ const styles = getComputedStyle(document.documentElement);
669
+ const isDark = document.documentElement.dataset.theme !== "light";
670
+ this.palette = {
671
+ isDark,
672
+ bg0: styles.getPropertyValue("--bg-0").trim(),
673
+ bg1: styles.getPropertyValue("--bg-1").trim(),
674
+ bg2: styles.getPropertyValue("--bg-2").trim(),
675
+ bg3: styles.getPropertyValue("--bg-3").trim(),
676
+ text: styles.getPropertyValue("--text-primary").trim(),
677
+ muted: styles.getPropertyValue("--text-muted").trim(),
678
+ border: styles.getPropertyValue("--border").trim(),
679
+ accent: styles.getPropertyValue("--accent").trim(),
680
+ success: styles.getPropertyValue("--success").trim(),
681
+ info: styles.getPropertyValue("--info").trim(),
682
+ warning: styles.getPropertyValue("--warning").trim(),
683
+ error: styles.getPropertyValue("--error").trim(),
684
+ floor: isDark ? "#252b31" : "#eceff2",
685
+ floorAlt: isDark ? "#2e353d" : "#dfe5ea",
686
+ wall: isDark ? "#151a1f" : "#fcfdff",
687
+ trim: isDark ? "#0d1115" : "#cfd6de",
688
+ desk: isDark ? "#4b5563" : "#d1d8e0",
689
+ deskTop: isDark ? "#667181" : "#e0e5eb",
690
+ chair: isDark ? "#1b232c" : "#8f9bab",
691
+ screen: isDark ? "#111827" : "#eff6ff",
692
+ screenGlow: isDark ? "#60a5fa" : "#2563eb",
693
+ glass: isDark ? "rgba(120, 162, 219, 0.18)" : "rgba(134, 189, 255, 0.28)",
694
+ plant: isDark ? "#4ca56f" : "#5fba7f",
695
+ plantDark: isDark ? "#2a6d48" : "#438e5f",
696
+ shadow: isDark ? "rgba(0,0,0,0.26)" : "rgba(59,76,94,0.10)",
697
+ paper: isDark ? "#dbeafe" : "#ffffff",
698
+ rug: isDark ? "#2f3640" : "#dde4eb",
699
+ rugLine: isDark ? "#3d4754" : "#c7d1db",
700
+ wood: isDark ? "#705038" : "#c28d62",
701
+ coffee: isDark ? "#2b211d" : "#6b4b38",
702
+ };
703
+ }
704
+
705
+ loadImage(src) {
706
+ const img = new Image();
707
+ img.src = src;
708
+ return img;
709
+ }
525
710
 
526
- class ActivityTimeline {
527
- constructor(feedEl) {
528
- this.feed = feedEl;
529
- this.steps = new Map(); // stepId → { el, cardEl }
530
- this.stepCount = 0;
711
+ refreshSummary() {
712
+ if (this.historyLoaded) return;
713
+ api("/agents?limit=6")
714
+ .then((data) => {
715
+ this.historyLoaded = true;
716
+ const runs = data.runs || [];
717
+ if (!runs.length || this.runMode !== "idle") return;
718
+ this.pushEvent("history", `${runs.length} recent runs archived in the background.`);
719
+ })
720
+ .catch(() => {});
721
+ }
722
+
723
+ resetForNewRun() {
724
+ this.runId = null;
725
+ this.runMode = "idle";
726
+ this.activeTool = null;
727
+ this.taskLabel = "No active run";
728
+ this.statusLabel = "Ambient systems nominal";
729
+ this.totalTools = 0;
730
+ this.stepAssignments.clear();
731
+ this.helpers = [];
732
+ this.packets.length = 0;
733
+ this.scanFlash = 0;
734
+ this.errorFlash = 0;
735
+ this.mainAgent.focus = "core";
736
+ this.mainAgent.status = "Waiting for the next task";
737
+ this.mainAgent.lastActive = this.tick;
738
+ this.renderAgents();
739
+ this.renderHud();
740
+ }
741
+
742
+ onRunStart(data) {
743
+ this.runId = data.runId;
744
+ this.runMode = "running";
745
+ this.activeTool = "Boot sequence";
746
+ this.scanFlash = 1;
747
+ this.mainAgent.status = "Welcoming a new task";
748
+ this.mainAgent.lastActive = this.tick;
749
+ this.pushEvent("run", `${data.title || `Run ${data.runId}`} is now live.`);
750
+ this.flashStructure("core", 1.2);
751
+ this.renderAgents();
752
+ this.renderHud(data.title || `Run ${data.runId}`, "NeoAgent is getting everything set up");
753
+ }
754
+
755
+ onThinking(data) {
756
+ this.runMode = "running";
757
+ this.activeTool = `Thinking step ${data.iteration}`;
758
+ this.scanFlash = Math.min(1.4, this.scanFlash + 0.18);
759
+ this.mainAgent.status = `Thinking through step ${data.iteration}`;
760
+ this.mainAgent.lastActive = this.tick;
761
+ this.pushEvent("think", `NeoAgent is thinking through step ${data.iteration}.`);
762
+ this.renderAgents();
763
+ this.renderHud(undefined, "NeoAgent is planning the next move");
764
+ }
765
+
766
+ onToolStart(data) {
767
+ this.runMode = "running";
768
+ this.activeTool = getToolMeta(data.toolName).label;
769
+ this.totalTools += 1;
770
+ const structureKey = this.getStructureForTool(data.toolName);
771
+ const actor = this.assignActor(data.stepId, data.toolName, data.toolArgs, structureKey);
772
+ const target = this.getStructure(structureKey);
773
+ this.spawnPacket(actor, target, target.color, data.toolName);
774
+ this.flashStructure(structureKey, 1.4);
775
+ this.pushEvent("tool", `${actor.name} is using ${getToolMeta(data.toolName).label.toLowerCase()} for ${this.getShortToolText(data.toolName, data.toolArgs)}.`);
776
+ this.renderAgents();
777
+ this.renderHud(undefined, `${actor.name} is working through ${target.label}`);
778
+ }
779
+
780
+ onToolEnd(data) {
781
+ const structureKey = this.getStructureForTool(data.toolName);
782
+ const actor = this.resolveActorForStep(data.stepId);
783
+ this.flashStructure(structureKey, data.status === "failed" ? 1.8 : 0.9);
784
+ if (data.status === "failed") {
785
+ this.runMode = "failed";
786
+ this.errorFlash = 1;
787
+ actor.status = "Hit an issue and is regrouping";
788
+ actor.lastActive = this.tick;
789
+ this.pushEvent("fault", `${actor.name} hit a snag while using ${getToolMeta(data.toolName).label.toLowerCase()}.`);
790
+ this.renderHud(undefined, `${actor.name} ran into an error`);
791
+ } else {
792
+ actor.status = `Wrapped up ${getToolMeta(data.toolName).label.toLowerCase()}`;
793
+ actor.lastActive = this.tick;
794
+ this.pushEvent("sync", `${actor.name} finished ${getToolMeta(data.toolName).label.toLowerCase()} cleanly.`);
795
+ this.renderHud(undefined, `${actor.name} finished successfully`);
796
+ }
797
+ this.renderAgents();
798
+ this.stepAssignments.delete(data.stepId);
799
+ }
800
+
801
+ onRunComplete(data) {
802
+ this.runMode = data.status === "failed" ? "failed" : "completed";
803
+ this.activeTool = data.status === "failed" ? "Recovery" : "Cooling down";
804
+ this.stepAssignments.clear();
805
+ this.mainAgent.status =
806
+ this.runMode === "failed" ? "Comforting the crew and recovering" : "Wrapping up with the crew";
807
+ this.mainAgent.lastActive = this.tick;
808
+ for (const helper of this.helpers) {
809
+ helper.status =
810
+ this.runMode === "failed" ? "Standing by for retry" : "Heading back after helping";
811
+ }
812
+ this.flashStructure("core", this.runMode === "failed" ? 1.6 : 1.2);
813
+ this.pushEvent(
814
+ this.runMode === "failed" ? "fault" : "done",
815
+ data.content ? data.content.slice(0, 90) : this.runMode === "failed" ? "The team is recovering from a failed run." : "The team finished the run."
816
+ );
817
+ this.renderAgents();
818
+ this.renderHud(undefined, this.runMode === "failed" ? "The crew is recovering from a rough run" : "The crew wrapped everything up nicely");
531
819
  }
532
820
 
533
- addNode(stepId, toolName, toolArgs) {
534
- if (this.steps.has(stepId)) return this.steps.get(stepId).el;
535
- this._clearEmpty();
536
- const meta = getToolMeta(toolName);
821
+ onRunError(data) {
822
+ this.runMode = "failed";
823
+ this.activeTool = "Recovery";
824
+ this.errorFlash = 1.1;
825
+ this.mainAgent.status = "Helping the crew recover";
826
+ for (const helper of this.helpers) {
827
+ helper.status = "Waiting for new instructions";
828
+ }
829
+ this.flashStructure("core", 1.6);
830
+ this.pushEvent("fault", data.error || "Unknown run error");
831
+ this.renderAgents();
832
+ this.renderHud(undefined, data.error || "The crew hit an unexpected error");
833
+ }
834
+
835
+ onMessage(data) {
836
+ this.totalMessages += 1;
837
+ this.socialPulse = 1.2;
838
+ this.flashStructure("social", 1.5);
839
+ const actor = this.helpers.find((helper) => helper.focus === "social") || this.mainAgent;
840
+ actor.status = "Greeting a new incoming message";
841
+ actor.lastActive = this.tick;
842
+ const target = this.getStructure("social");
843
+ this.spawnPacket(actor, target, "#ff7db7", "message");
844
+ this.pushEvent("msg", `${actor.name} noticed a ${data.platform} message: ${String(data.content || "").slice(0, 72)}`);
845
+ this.renderAgents();
846
+ this.renderHud(undefined, "A friendly ping just reached the message port");
847
+ }
848
+
849
+ getShortToolText(toolName, toolArgs) {
537
850
  const desc = describeArgs(toolName, toolArgs);
851
+ return desc?.headline ? desc.headline.slice(0, 58) : "signal received";
852
+ }
538
853
 
539
- const stepEl = document.createElement("div");
540
- stepEl.className = `atl-step running`;
541
- stepEl.dataset.color = meta.color;
542
- stepEl.id = `atl-step-${stepId}`;
854
+ getStructureForTool(toolName) {
855
+ if (toolName.startsWith("browser_")) return "browser";
856
+ if (toolName.startsWith("memory_")) return "memory";
857
+ if (toolName === "execute_command") return "cli";
858
+ if (toolName === "send_message" || toolName === "make_call") return "social";
859
+ return "core";
860
+ }
543
861
 
544
- const summaryText = desc?.headline
545
- ? escapeHtml(desc.headline.slice(0, 120))
546
- : escapeHtml(meta.label);
862
+ getStructure(key) {
863
+ return this.structures.find((item) => item.key === key) || this.structures[0];
864
+ }
547
865
 
548
- let urlChip = "";
549
- if (toolName === "browser_navigate" && toolArgs?.url) {
550
- const u = toolArgs.url;
551
- urlChip = `<a class="atl-url-chip" href="${escapeHtml(u)}" target="_blank" rel="noopener noreferrer">${escapeHtml(u.length > 80 ? u.slice(0, 80) + "…" : u)}</a>`;
866
+ assignActor(stepId, toolName, toolArgs, structureKey) {
867
+ if (toolName === "spawn_subagent") {
868
+ const helper = this.spawnHelper(toolArgs, structureKey);
869
+ this.stepAssignments.set(stepId, helper.id);
870
+ this.mainAgent.status = `Delegating work to ${helper.name}`;
871
+ this.mainAgent.lastActive = this.tick;
872
+ return helper;
552
873
  }
553
874
 
554
- stepEl.innerHTML = `
555
- <div class="atl-spine">
556
- <div class="atl-dot">${meta.icon}</div>
557
- <div class="atl-connector"></div>
558
- </div>
559
- <div class="atl-card open" id="atl-card-${stepId}">
560
- <div class="atl-card-head" data-step="${stepId}">
561
- <span class="atl-card-label">${escapeHtml(meta.label)}</span>
562
- <span class="atl-card-summary">${summaryText}</span>
563
- <span class="atl-status-chip running" id="atl-chip-${stepId}">running</span>
564
- <span class="atl-toggle">▾</span>
875
+ const specialist = this.helpers
876
+ .filter((helper) => helper.focus === structureKey)
877
+ .sort((a, b) => a.lastActive - b.lastActive)[0];
878
+ const actor = specialist || this.mainAgent;
879
+ actor.focus = structureKey;
880
+ actor.status = this.describeFriendlyAction(toolName, toolArgs);
881
+ actor.lastActive = this.tick;
882
+ this.stepAssignments.set(stepId, actor.id);
883
+ return actor;
884
+ }
885
+
886
+ resolveActorForStep(stepId) {
887
+ const actorId = this.stepAssignments.get(stepId);
888
+ if (!actorId || actorId === this.mainAgent.id) return this.mainAgent;
889
+ return this.helpers.find((helper) => helper.id === actorId) || this.mainAgent;
890
+ }
891
+
892
+ spawnHelper(toolArgs, structureKey) {
893
+ const slot = this.helperSlots[this.helpers.length % this.helperSlots.length];
894
+ this.helperCounter += 1;
895
+ const specialty = this.inferHelperSpecialty(toolArgs, structureKey);
896
+ const focus = this.inferHelperFocus(toolArgs, structureKey);
897
+ const helper = {
898
+ id: `helper-${this.helperCounter}`,
899
+ name: `Scout-${this.helperCounter}`,
900
+ type: "helper",
901
+ x: slot.x,
902
+ y: slot.y,
903
+ tint: ["#9fd6ff", "#ffd36b", "#ff9dce", "#cdb8ff"][this.helperCounter % 4],
904
+ phase: 0.5 + this.helperCounter,
905
+ focus,
906
+ specialty,
907
+ status: `Joining the task to help with ${specialty.toLowerCase()}`,
908
+ lastActive: this.tick,
909
+ };
910
+ this.helpers.push(helper);
911
+ this.spawnPacket(this.mainAgent, helper, helper.tint, "delegate");
912
+ this.pushEvent("team", `NeoAgent spawned ${helper.name} to help with ${specialty.toLowerCase()}.`);
913
+ return helper;
914
+ }
915
+
916
+ inferHelperSpecialty(toolArgs, structureKey) {
917
+ const headline =
918
+ toolArgs?.task ||
919
+ toolArgs?.prompt ||
920
+ toolArgs?.description ||
921
+ toolArgs?.content ||
922
+ this.getStructure(structureKey).label;
923
+ return String(headline).slice(0, 36);
924
+ }
925
+
926
+ inferHelperFocus(toolArgs, fallback) {
927
+ const text = JSON.stringify(toolArgs || {}).toLowerCase();
928
+ if (text.includes("browser") || text.includes("web") || text.includes("search") || text.includes("page")) return "browser";
929
+ if (text.includes("memory") || text.includes("recall") || text.includes("history")) return "memory";
930
+ if (text.includes("command") || text.includes("shell") || text.includes("terminal") || text.includes("file")) return "cli";
931
+ if (text.includes("message") || text.includes("call") || text.includes("email")) return "social";
932
+ return fallback;
933
+ }
934
+
935
+ describeFriendlyAction(toolName, toolArgs) {
936
+ const headline = this.getShortToolText(toolName, toolArgs);
937
+ if (toolName === "execute_command") return `Checking the command forge for ${headline}`;
938
+ if (toolName.startsWith("browser_")) return `Exploring the web for ${headline}`;
939
+ if (toolName.startsWith("memory_")) return `Digging through memory for ${headline}`;
940
+ if (toolName === "send_message" || toolName === "make_call") return `Reaching out about ${headline}`;
941
+ return `Working on ${headline}`;
942
+ }
943
+
944
+ spawnPacket(bot, structure, color, label) {
945
+ const targetX = structure.x + Math.floor((structure.w || 2) / 2);
946
+ const targetY = structure.y + ((structure.h || 2) > 2 ? 4 : 0);
947
+ this.packets.push({
948
+ x: bot.x,
949
+ y: bot.y - 4,
950
+ fromX: bot.x,
951
+ fromY: bot.y - 4,
952
+ toX: targetX,
953
+ toY: targetY,
954
+ color,
955
+ label,
956
+ progress: 0,
957
+ speed: 0.018 + Math.random() * 0.018,
958
+ });
959
+ }
960
+
961
+ flashStructure(key, amount) {
962
+ const structure = this.getStructure(key);
963
+ structure.glow = Math.max(structure.glow, amount);
964
+ }
965
+
966
+ pushEvent(tag, text) {
967
+ this.recentEvents.unshift({
968
+ tag,
969
+ text,
970
+ time: new Date(),
971
+ });
972
+ this.recentEvents = this.recentEvents.slice(0, 6);
973
+ if (this.ui.badge) this.ui.badge.classList.remove("hidden");
974
+ this.renderEventList();
975
+ }
976
+
977
+ renderAgents() {
978
+ const list = this.ui.agents;
979
+ if (!list) return;
980
+ const roster = [this.mainAgent, ...this.helpers];
981
+ list.innerHTML = roster
982
+ .map((agent) => `
983
+ <div class="world-agent-card">
984
+ <div class="world-agent-topline">
985
+ <span class="world-agent-title">${escapeHtml(agent.name)}</span>
986
+ <span class="world-agent-chip ${agent.type === "lead" ? "lead" : "helper"}">${agent.type === "lead" ? "Lead" : "Helper"}</span>
987
+ </div>
988
+ <div class="world-agent-meta">${escapeHtml(agent.specialty)}</div>
989
+ <div class="world-agent-status">${escapeHtml(agent.status)}</div>
565
990
  </div>
566
- <div class="atl-card-body">
567
- ${desc?.headline ? `<div class="atl-cmd">${escapeHtml(desc.headline)}</div>` : ""}
568
- ${desc?.detail ? `<div class="atl-detail">${escapeHtml(desc.detail)}</div>` : ""}
569
- ${urlChip}
570
- <div id="atl-result-${stepId}"></div>
991
+ `)
992
+ .join("");
993
+ }
994
+
995
+ renderEventList() {
996
+ const list = this.ui.events;
997
+ if (!list) return;
998
+ if (!this.recentEvents.length) {
999
+ list.innerHTML = '<div class="world-empty-state">The world is idling. Start a task in chat to wake everything up.</div>';
1000
+ return;
1001
+ }
1002
+ list.innerHTML = this.recentEvents
1003
+ .map((event) => `
1004
+ <div class="world-event-entry">
1005
+ <div class="world-event-topline">
1006
+ <span class="world-event-tag">${escapeHtml(event.tag)}</span>
1007
+ <span class="world-event-time">${escapeHtml(formatTime(event.time))}</span>
1008
+ </div>
1009
+ <div class="world-event-text">${escapeHtml(event.text)}</div>
571
1010
  </div>
572
- </div>`;
1011
+ `)
1012
+ .join("");
1013
+ }
1014
+
1015
+ renderHud(taskText, statusText) {
1016
+ if (taskText) this.taskLabel = taskText;
1017
+ if (statusText) this.statusLabel = statusText;
1018
+ const modeText =
1019
+ this.runMode === "running"
1020
+ ? "Running"
1021
+ : this.runMode === "completed"
1022
+ ? "Complete"
1023
+ : this.runMode === "failed"
1024
+ ? "Fault"
1025
+ : "Idle";
1026
+ if (this.ui.modePill) this.ui.modePill.textContent = modeText;
1027
+ if (this.ui.toolPill) this.ui.toolPill.textContent = this.activeTool || "Awaiting signal";
1028
+ if (this.ui.task) this.ui.task.textContent = this.taskLabel || (this.runId ? `Run ${this.runId}` : "No active run");
1029
+ if (this.ui.status) this.ui.status.textContent = this.statusLabel || this.getAmbientStatus();
1030
+ if (this.ui.mode) this.ui.mode.textContent = modeText;
1031
+ if (this.ui.run) this.ui.run.textContent = this.runId ? String(this.runId) : "None";
1032
+ if (this.ui.tools) this.ui.tools.textContent = String(this.totalTools);
1033
+ if (this.ui.helpers) this.ui.helpers.textContent = String(this.helpers.length);
1034
+ if (this.ui.messages) this.ui.messages.textContent = String(this.totalMessages);
1035
+ }
1036
+
1037
+ getAmbientStatus() {
1038
+ if (this.runMode === "running") return "The office is busy and everyone is moving work forward";
1039
+ if (this.runMode === "completed") return "The office settled down after a clean handoff";
1040
+ if (this.runMode === "failed") return "The team is regrouping after a rough patch";
1041
+ return "The office is quiet and ready for the next task";
1042
+ }
1043
+
1044
+ loop() {
1045
+ this.tick += 1;
1046
+ this.updateSimulation();
1047
+ this.draw();
1048
+ requestAnimationFrame(this.loop);
1049
+ }
1050
+
1051
+ updateSimulation() {
1052
+ this.scanFlash *= 0.97;
1053
+ this.socialPulse *= 0.94;
1054
+ this.errorFlash *= 0.93;
1055
+
1056
+ for (const structure of this.structures) {
1057
+ structure.glow *= 0.94;
1058
+ }
573
1059
 
574
- this.feed.appendChild(stepEl);
575
- this.steps.set(stepId, {
576
- el: stepEl,
577
- cardEl: stepEl.querySelector(`#atl-card-${stepId}`),
578
- isResponse: false,
579
- });
580
- this.stepCount++;
1060
+ this.mainAgent.phase += 0.025;
1061
+ for (const helper of this.helpers) helper.phase += 0.025;
581
1062
 
582
- // Toggle open/close on header click
583
- stepEl.querySelector(".atl-card-head").addEventListener("click", () => {
584
- stepEl.querySelector(`#atl-card-${stepId}`).classList.toggle("open");
1063
+ this.packets = this.packets.filter((packet) => {
1064
+ packet.progress += packet.speed;
1065
+ packet.x = packet.fromX + (packet.toX - packet.fromX) * packet.progress;
1066
+ packet.y = packet.fromY + (packet.toY - packet.fromY) * packet.progress - Math.sin(packet.progress * Math.PI) * 10;
1067
+ return packet.progress < 1;
585
1068
  });
586
-
587
- stepEl.scrollIntoView({ behavior: "smooth", block: "nearest" });
588
- return stepEl;
589
1069
  }
590
1070
 
591
- updateNode(stepId, toolName, result, screenshotPath, status) {
592
- const info = this.steps.get(stepId);
593
- if (!info) return;
1071
+ draw() {
1072
+ const ctx = this.bctx;
1073
+ const t = this.tick;
1074
+ ctx.clearRect(0, 0, 384, 216);
1075
+ this.drawOffice(ctx);
1076
+ this.drawStructures(ctx);
1077
+ this.drawWalkways(ctx);
1078
+ this.drawBots(ctx, t);
1079
+ this.drawPackets(ctx);
1080
+ this.drawStatusEffects(ctx, t);
1081
+ this.drawScanlines(ctx);
1082
+
1083
+ this.ctx.save();
1084
+ this.ctx.setTransform(1, 0, 0, 1, 0, 0);
1085
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
1086
+ this.ctx.imageSmoothingEnabled = false;
1087
+ this.ctx.drawImage(this.buffer, 0, 0, this.canvas.width, this.canvas.height);
1088
+ this.ctx.restore();
1089
+ }
1090
+
1091
+ drawOffice(ctx) {
1092
+ const ref = this.palette.isDark ? this.officeImages.dark : this.officeImages.light;
1093
+ if (ref && ref.complete && ref.naturalWidth > 0) {
1094
+ this.drawReferenceImage(ctx, ref, 384, 216);
1095
+ return;
1096
+ }
1097
+
1098
+ const p = this.palette;
1099
+ ctx.fillStyle = p.floor;
1100
+ ctx.fillRect(0, 0, 384, 216);
594
1101
 
595
- const chip = document.getElementById(`atl-chip-${stepId}`);
596
- if (chip) {
597
- chip.className = `atl-status-chip ${status}`;
598
- if (status === "completed") {
599
- chip.textContent = "done";
600
- } else if (status === "failed") {
601
- chip.textContent = "failed";
602
- } else {
603
- chip.textContent = status; // Keep "running" or "pending"
1102
+ ctx.fillStyle = p.floorAlt;
1103
+ for (let y = 0; y < 216; y += 24) {
1104
+ for (let x = (y / 24) % 2 === 0 ? 0 : 12; x < 384; x += 24) {
1105
+ ctx.fillRect(x, y, 12, 12);
604
1106
  }
605
1107
  }
606
1108
 
607
- if (status === "completed" || status === "failed") {
608
- info.el.classList.remove("running");
609
- }
610
- if (status === "failed") info.el.dataset.color = "tool";
611
-
612
- const resultEl = document.getElementById(`atl-result-${stepId}`);
613
- if (!resultEl) return;
614
-
615
- // Screenshot
616
- if (screenshotPath) {
617
- const wrap = document.createElement("div");
618
- wrap.className = "atl-screenshot-wrap";
619
- const a = document.createElement("a");
620
- a.href = screenshotPath;
621
- a.target = "_blank";
622
- a.rel = "noopener noreferrer";
623
- const img = document.createElement("img");
624
- img.className = "atl-screenshot";
625
- img.src = screenshotPath;
626
- img.alt = "";
627
- img.loading = "lazy";
628
- a.appendChild(img);
629
- wrap.appendChild(a);
630
- resultEl.appendChild(wrap);
631
- }
632
-
633
- const rd = describeResult(toolName, result);
634
- if (rd) {
635
- if (rd.meta) {
636
- const m = document.createElement("div");
637
- m.className = rd.statusClass
638
- ? `atl-http-badge ${rd.statusClass}`
639
- : `atl-result-label${rd.type === "error" ? " error" : ""}`;
640
- m.textContent = rd.meta;
641
- resultEl.appendChild(m);
642
- }
643
- if (rd.text) {
644
- const d = document.createElement("div");
645
- d.className =
646
- rd.type === "code"
647
- ? "atl-code"
648
- : rd.type === "error"
649
- ? "atl-text error"
650
- : rd.type === "success"
651
- ? "atl-success"
652
- : "atl-text";
653
- d.textContent = rd.text;
654
- resultEl.appendChild(d);
655
- }
1109
+ ctx.fillStyle = p.wall;
1110
+ ctx.fillRect(12, 12, 360, 10);
1111
+ ctx.fillRect(12, 194, 360, 10);
1112
+ ctx.fillRect(12, 22, 10, 172);
1113
+ ctx.fillRect(362, 22, 10, 172);
1114
+ ctx.fillStyle = p.trim;
1115
+ ctx.fillRect(22, 22, 340, 2);
1116
+ ctx.fillRect(22, 192, 340, 2);
1117
+ ctx.fillRect(22, 22, 2, 170);
1118
+ ctx.fillRect(360, 22, 2, 170);
1119
+
1120
+ ctx.fillStyle = p.glass;
1121
+ this.drawWindow(ctx, 118, 22, 62, 10);
1122
+ this.drawWindow(ctx, 190, 22, 62, 10);
1123
+
1124
+ ctx.fillStyle = p.rug;
1125
+ ctx.fillRect(134, 68, 116, 74);
1126
+ ctx.fillStyle = p.rugLine;
1127
+ for (let x = 140; x < 240; x += 12) ctx.fillRect(x, 74, 2, 62);
1128
+
1129
+ this.drawMeetingTable(ctx, 146, 80);
1130
+ this.drawShelfWall(ctx, 34, 34);
1131
+ this.drawShelfWall(ctx, 318, 34);
1132
+ this.drawCoffeeBar(ctx, 300, 150);
1133
+ this.drawPrinterNook(ctx, 34, 150);
1134
+ this.drawLounge(ctx, 168, 150);
1135
+
1136
+ this.drawPlant(ctx, 30, 30);
1137
+ this.drawPlant(ctx, 346, 30);
1138
+ this.drawPlant(ctx, 30, 178);
1139
+ this.drawPlant(ctx, 346, 178);
1140
+
1141
+ this.drawDeskCluster(ctx, 154, 80, "core");
1142
+ this.drawDeskCluster(ctx, 270, 42, "browser");
1143
+ this.drawDeskCluster(ctx, 56, 42, "memory");
1144
+ this.drawDeskCluster(ctx, 56, 146, "cli");
1145
+ this.drawDeskCluster(ctx, 270, 146, "social");
1146
+ }
1147
+
1148
+ drawReferenceImage(ctx, image, targetWidth, targetHeight) {
1149
+ const imageRatio = image.naturalWidth / image.naturalHeight;
1150
+ const targetRatio = targetWidth / targetHeight;
1151
+
1152
+ let drawWidth = targetWidth;
1153
+ let drawHeight = targetHeight;
1154
+ let offsetX = 0;
1155
+ let offsetY = 0;
1156
+
1157
+ if (imageRatio > targetRatio) {
1158
+ drawWidth = targetWidth;
1159
+ drawHeight = Math.round(targetWidth / imageRatio);
1160
+ offsetY = Math.floor((targetHeight - drawHeight) / 2);
1161
+ } else {
1162
+ drawHeight = targetHeight;
1163
+ drawWidth = Math.round(targetHeight * imageRatio);
1164
+ offsetX = Math.floor((targetWidth - drawWidth) / 2);
656
1165
  }
657
1166
 
658
- // Update summary with result snippet
659
- const summary = info.el.querySelector(".atl-card-summary");
660
- if (summary && rd?.text && rd.type !== "error") {
661
- const snippet = rd.text.split("\n")[0].slice(0, 80);
662
- if (snippet) summary.textContent = snippet;
1167
+ ctx.fillStyle = this.palette.floor;
1168
+ ctx.fillRect(0, 0, targetWidth, targetHeight);
1169
+ ctx.drawImage(image, offsetX, offsetY, drawWidth, drawHeight);
1170
+ }
1171
+
1172
+ drawPlant(ctx, x, y) {
1173
+ const p = this.palette;
1174
+ ctx.fillStyle = p.wood;
1175
+ ctx.fillRect(x, y + 8, 8, 7);
1176
+ ctx.fillStyle = p.plantDark;
1177
+ ctx.fillRect(x - 2, y + 2, 12, 8);
1178
+ ctx.fillStyle = p.plant;
1179
+ ctx.fillRect(x - 4, y, 16, 7);
1180
+ }
1181
+
1182
+ drawWindow(ctx, x, y, w, h) {
1183
+ const p = this.palette;
1184
+ ctx.fillStyle = p.trim;
1185
+ ctx.fillRect(x, y, w, h);
1186
+ ctx.fillStyle = p.glass;
1187
+ ctx.fillRect(x + 2, y + 2, w - 4, h - 4);
1188
+ ctx.fillStyle = this.hexToRgba("#ffffff", p.isDark ? 0.12 : 0.3);
1189
+ ctx.fillRect(x + 8, y + 2, 2, h - 4);
1190
+ ctx.fillRect(x + 24, y + 2, 2, h - 4);
1191
+ ctx.fillRect(x + 40, y + 2, 2, h - 4);
1192
+ }
1193
+
1194
+ drawMeetingTable(ctx, x, y) {
1195
+ const p = this.palette;
1196
+ ctx.fillStyle = p.shadow;
1197
+ ctx.fillRect(x + 4, y + 38, 92, 4);
1198
+ ctx.fillStyle = p.wood;
1199
+ ctx.fillRect(x, y, 100, 38);
1200
+ ctx.fillStyle = p.paper;
1201
+ ctx.fillRect(x + 12, y + 10, 18, 10);
1202
+ ctx.fillRect(x + 68, y + 10, 18, 10);
1203
+ ctx.fillStyle = p.chair;
1204
+ ctx.fillRect(x - 8, y + 6, 8, 10);
1205
+ ctx.fillRect(x - 8, y + 22, 8, 10);
1206
+ ctx.fillRect(x + 100, y + 6, 8, 10);
1207
+ ctx.fillRect(x + 100, y + 22, 8, 10);
1208
+ }
1209
+
1210
+ drawShelfWall(ctx, x, y) {
1211
+ const p = this.palette;
1212
+ ctx.fillStyle = p.trim;
1213
+ ctx.fillRect(x, y, 34, 44);
1214
+ ctx.fillStyle = p.wood;
1215
+ for (let y0 = y + 4; y0 < y + 40; y0 += 12) {
1216
+ ctx.fillRect(x + 2, y0, 30, 2);
1217
+ }
1218
+ ctx.fillStyle = p.warning;
1219
+ ctx.fillRect(x + 5, y + 7, 5, 7);
1220
+ ctx.fillRect(x + 12, y + 7, 4, 7);
1221
+ ctx.fillRect(x + 18, y + 7, 6, 7);
1222
+ ctx.fillRect(x + 8, y + 19, 6, 7);
1223
+ ctx.fillRect(x + 18, y + 19, 4, 7);
1224
+ ctx.fillRect(x + 12, y + 31, 5, 7);
1225
+ ctx.fillRect(x + 20, y + 31, 7, 7);
1226
+ }
1227
+
1228
+ drawCoffeeBar(ctx, x, y) {
1229
+ const p = this.palette;
1230
+ ctx.fillStyle = p.deskTop;
1231
+ ctx.fillRect(x, y, 44, 24);
1232
+ ctx.fillStyle = p.coffee;
1233
+ ctx.fillRect(x + 4, y + 4, 14, 12);
1234
+ ctx.fillStyle = p.paper;
1235
+ ctx.fillRect(x + 24, y + 5, 6, 8);
1236
+ ctx.fillRect(x + 32, y + 7, 5, 6);
1237
+ }
1238
+
1239
+ drawPrinterNook(ctx, x, y) {
1240
+ const p = this.palette;
1241
+ ctx.fillStyle = p.deskTop;
1242
+ ctx.fillRect(x, y, 42, 24);
1243
+ ctx.fillStyle = p.screen;
1244
+ ctx.fillRect(x + 7, y + 5, 20, 10);
1245
+ ctx.fillStyle = p.paper;
1246
+ ctx.fillRect(x + 12, y + 2, 10, 5);
1247
+ ctx.fillRect(x + 30, y + 8, 7, 5);
1248
+ }
1249
+
1250
+ drawLounge(ctx, x, y) {
1251
+ const p = this.palette;
1252
+ ctx.fillStyle = p.rug;
1253
+ ctx.fillRect(x, y, 48, 30);
1254
+ ctx.fillStyle = p.chair;
1255
+ ctx.fillRect(x + 4, y + 8, 14, 14);
1256
+ ctx.fillRect(x + 30, y + 8, 14, 14);
1257
+ ctx.fillStyle = p.wood;
1258
+ ctx.fillRect(x + 20, y + 11, 8, 8);
1259
+ }
1260
+
1261
+ drawDeskCluster(ctx, x, y, key) {
1262
+ const p = this.palette;
1263
+ const structure = this.getStructure(key);
1264
+ ctx.fillStyle = p.shadow;
1265
+ ctx.fillRect(x - 4, y + structure.h + 2, structure.w + 8, 4);
1266
+ ctx.fillStyle = p.deskTop;
1267
+ ctx.fillRect(x, y, structure.w, structure.h);
1268
+ ctx.fillStyle = p.desk;
1269
+ ctx.fillRect(x + 4, y + 4, structure.w - 8, structure.h - 8);
1270
+ ctx.fillStyle = p.screen;
1271
+ ctx.fillRect(x + 8, y + 8, structure.w - 16, 10);
1272
+ ctx.fillStyle = p.screenGlow;
1273
+ ctx.fillRect(x + 10, y + 10, structure.w - 20, 4);
1274
+ ctx.fillStyle = p.paper;
1275
+ ctx.fillRect(x + 10, y + structure.h - 12, 10, 6);
1276
+ ctx.fillStyle = p.chair;
1277
+ ctx.fillRect(x + Math.floor(structure.w / 2) - 8, y + structure.h + 2, 16, 8);
1278
+ if (key === "memory") {
1279
+ ctx.fillStyle = p.warning;
1280
+ ctx.fillRect(x + structure.w - 18, y + 22, 10, 6);
1281
+ } else if (key === "browser") {
1282
+ ctx.fillStyle = p.info;
1283
+ ctx.fillRect(x + structure.w - 18, y + 22, 10, 6);
1284
+ } else if (key === "social") {
1285
+ ctx.fillStyle = "#ec4899";
1286
+ ctx.fillRect(x + structure.w - 18, y + 22, 10, 6);
1287
+ } else if (key === "cli") {
1288
+ ctx.fillStyle = "#f97316";
1289
+ ctx.fillRect(x + structure.w - 18, y + 22, 10, 6);
1290
+ } else {
1291
+ ctx.fillStyle = p.success;
1292
+ ctx.fillRect(x + structure.w - 18, y + 22, 10, 6);
663
1293
  }
664
1294
  }
665
1295
 
666
- addResponse(content) {
667
- if (!content) return;
668
- this._clearEmpty();
669
-
670
- const fakeId = `__resp_${Date.now()}`;
671
- const stepEl = document.createElement("div");
672
- stepEl.className = "atl-step";
673
- stepEl.dataset.color = "response";
674
- stepEl.id = `atl-step-${fakeId}`;
675
- stepEl.innerHTML = `
676
- <div class="atl-spine">
677
- <div class="atl-dot">✅</div>
678
- </div>
679
- <div class="atl-card open" id="atl-card-${fakeId}">
680
- <div class="atl-card-head" data-step="${fakeId}">
681
- <span class="atl-card-label">Response</span>
682
- <span class="atl-card-summary" style="font-style:italic;color:var(--text-muted);">final answer</span>
683
- <span class="atl-toggle">▾</span>
684
- </div>
685
- <div class="atl-card-body">
686
- <div class="atl-response-body md-content">${renderMarkdown(content)}</div>
687
- </div>
688
- </div>`;
689
-
690
- this.feed.appendChild(stepEl);
691
- this.steps.set(fakeId, {
692
- el: stepEl,
693
- cardEl: stepEl.querySelector(`#atl-card-${fakeId}`),
694
- isResponse: true,
695
- });
696
-
697
- stepEl.querySelector(".atl-card-head").addEventListener("click", () => {
698
- stepEl.querySelector(`#atl-card-${fakeId}`).classList.toggle("open");
699
- });
700
-
701
- stepEl.scrollIntoView({ behavior: "smooth", block: "nearest" });
1296
+ drawStructures(ctx) {
1297
+ for (const structure of this.structures) {
1298
+ const glowSize = Math.floor(structure.glow * 7);
1299
+ if (glowSize > 0) {
1300
+ ctx.fillStyle = this.hexToRgba(structure.color, 0.18);
1301
+ ctx.fillRect(structure.x - glowSize, structure.y - glowSize, structure.w + glowSize * 2, structure.h + glowSize * 2);
1302
+ }
1303
+ if (structure.glow > 0.08) {
1304
+ ctx.fillStyle = this.hexToRgba(structure.color, 0.18 + structure.glow * 0.08);
1305
+ ctx.fillRect(structure.x, structure.y, structure.w, 3);
1306
+ }
1307
+ }
702
1308
  }
703
1309
 
704
- _clearEmpty() {
705
- const e = document.getElementById("activityEmpty");
706
- if (e) e.style.display = "none";
1310
+ drawWalkways(ctx) {
1311
+ const core = this.getStructure("core");
1312
+ for (const structure of this.structures) {
1313
+ if (structure.key === "core") continue;
1314
+ const x1 = core.x + Math.floor(core.w / 2);
1315
+ const y1 = core.y + core.h - 2;
1316
+ const x2 = structure.x + Math.floor(structure.w / 2);
1317
+ const y2 = structure.y + Math.floor(structure.h / 2);
1318
+ ctx.fillStyle = this.hexToRgba(structure.color, 0.06 + structure.glow * 0.08);
1319
+ for (let x = Math.min(x1, x2); x <= Math.max(x1, x2); x += 6) {
1320
+ ctx.fillRect(x, y1, 2, 2);
1321
+ }
1322
+ for (let y = Math.min(y1, y2); y <= Math.max(y1, y2); y += 6) {
1323
+ ctx.fillRect(x2, y, 2, 2);
1324
+ }
1325
+ }
707
1326
  }
708
1327
 
709
- clear() {
710
- this.steps.clear();
711
- this.stepCount = 0;
712
- // Remove everything except the empty state placeholder
713
- const empty = document.getElementById("activityEmpty");
714
- this.feed.innerHTML = "";
715
- if (empty) {
716
- this.feed.appendChild(empty);
1328
+ drawBots(ctx, t) {
1329
+ const leadBounce = Math.sin(t * 0.08 + this.mainAgent.phase) > 0 ? 0 : 1;
1330
+ this.drawBot(ctx, this.mainAgent.x, this.mainAgent.y - leadBounce, this.mainAgent.tint, true, true);
1331
+ for (const helper of this.helpers) {
1332
+ const bounce = Math.sin(t * 0.08 + helper.phase) > 0 ? 0 : 1;
1333
+ this.drawBot(ctx, helper.x, helper.y - bounce, helper.tint, helper.lastActive + 80 > this.tick, false);
717
1334
  }
718
1335
  }
719
- }
720
-
721
- let activityTimeline = null;
722
- let currentActivityRunId = null;
723
- let currentActivityTimer = null;
724
- let currentActivityStartTs = null;
725
-
726
- function startRunTimer() {
727
- clearInterval(currentActivityTimer);
728
- currentActivityStartTs = Date.now();
729
- const el = $("#atlTimer");
730
- if (!el) return;
731
- el.style.display = "inline-block";
732
- el.textContent = "0s";
733
- currentActivityTimer = setInterval(() => {
734
- const s = Math.round((Date.now() - currentActivityStartTs) / 1000);
735
- el.textContent = s < 60 ? `${s}s` : `${Math.floor(s / 60)}m ${s % 60}s`;
736
- }, 1000);
737
- }
738
-
739
- function stopRunTimer() {
740
- clearInterval(currentActivityTimer);
741
- }
742
-
743
- function ensureTimeline() {
744
- if (activityTimeline) return;
745
- const feed = document.getElementById("activityFeed");
746
- if (!feed) return;
747
- activityTimeline = new ActivityTimeline(feed);
748
- }
749
-
750
- function addActivityNode(stepId, toolName, toolArgs, runId) {
751
- if (runId && currentActivityRunId !== runId) return;
752
- ensureTimeline();
753
- activityTimeline.addNode(stepId, toolName, toolArgs);
754
- const badge = $("#activityBadge");
755
- if (badge) badge.classList.remove("hidden");
756
- }
757
-
758
- function updateActivityNode(stepId, toolName, result, screenshotPath, status, runId) {
759
- if (runId && currentActivityRunId !== runId) return;
760
- if (activityTimeline)
761
- activityTimeline.updateNode(
762
- stepId,
763
- toolName,
764
- result,
765
- screenshotPath,
766
- status,
767
- );
768
- }
769
-
770
- function addActivityResponse(content, runId) {
771
- if (runId && currentActivityRunId !== runId) return;
772
- ensureTimeline();
773
- if (content) activityTimeline.addResponse(content);
774
- }
775
-
776
- function clearActivity() {
777
- if (activityTimeline) activityTimeline.clear();
778
1336
 
779
- const empty = $("#activityEmpty");
780
- if (empty) {
781
- if (currentActivityRunId) {
782
- empty.style.display = "none";
783
- } else {
784
- empty.style.display = "";
1337
+ drawBot(ctx, x, y, tint, active, isLead = false) {
1338
+ const p = this.palette;
1339
+ if (isLead) {
1340
+ ctx.fillStyle = this.hexToRgba(p.success, 0.18);
1341
+ ctx.fillRect(x - 12, y - 15, 24, 20);
1342
+ }
1343
+ ctx.fillStyle = p.shadow;
1344
+ ctx.fillRect(x - 8, y + 8, 16, 4);
1345
+ ctx.fillStyle = "#111318";
1346
+ ctx.fillRect(x - 7, y - 6, 14, 12);
1347
+ ctx.fillStyle = tint;
1348
+ ctx.fillRect(x - 6, y - 5, 12, 10);
1349
+ ctx.fillStyle = this.hexToRgba("#ffffff", 0.18);
1350
+ ctx.fillRect(x - 5, y - 4, 10, 2);
1351
+ ctx.fillStyle = p.paper;
1352
+ ctx.fillRect(x - 3, y - 1, 2, 2);
1353
+ ctx.fillRect(x + 1, y - 1, 2, 2);
1354
+ ctx.fillStyle = active ? p.success : p.chair;
1355
+ ctx.fillRect(x - 4, y + 5, 8, 3);
1356
+ ctx.fillRect(x - 7, y + 1, 3, 2);
1357
+ ctx.fillRect(x + 4, y + 1, 3, 2);
1358
+ ctx.fillStyle = "#111318";
1359
+ ctx.fillRect(x - 4, y + 8, 2, 3);
1360
+ ctx.fillRect(x + 2, y + 8, 2, 3);
1361
+ if (isLead) {
1362
+ ctx.fillStyle = p.success;
1363
+ ctx.fillRect(x - 2, y - 9, 4, 3);
785
1364
  }
786
1365
  }
787
- }
788
1366
 
789
- // ── Activity History Panel ──
1367
+ drawPackets(ctx) {
1368
+ for (const packet of this.packets) {
1369
+ ctx.fillStyle = packet.color;
1370
+ ctx.fillRect(Math.round(packet.x), Math.round(packet.y), 4, 4);
1371
+ ctx.fillStyle = this.palette.paper;
1372
+ ctx.fillRect(Math.round(packet.x) + 1, Math.round(packet.y) + 1, 2, 2);
1373
+ }
1374
+ }
790
1375
 
791
- async function loadActivityHistory() {
792
- const list = $("#activitySidebarList");
793
- if (!list) return;
794
- list.innerHTML = '<div class="activity-empty-text">Loading runs…</div>';
795
- try {
796
- const data = await api("/agents?limit=30");
797
- if (!data.runs || data.runs.length === 0) {
798
- list.innerHTML = '<div class="activity-empty-text">No past runs</div>';
799
- return;
1376
+ drawStatusEffects(ctx, t) {
1377
+ const p = this.palette;
1378
+ if (this.scanFlash > 0.02) {
1379
+ ctx.fillStyle = this.hexToRgba(p.info, Math.min(0.08, this.scanFlash * 0.06));
1380
+ const x = 24 + ((t * 2) % 320);
1381
+ ctx.fillRect(x, 20, 8, 176);
800
1382
  }
801
- list.innerHTML = "";
802
- for (const run of data.runs) {
803
- const card = document.createElement("div");
804
- card.className = "ahp-run-card";
805
- if (currentActivityRunId === run.id) card.classList.add("active");
806
- card.dataset.runId = run.id;
807
- const d = new Date(run.created_at);
808
- const dateStr =
809
- d.toLocaleDateString("en-US", { month: "short", day: "numeric" }) +
810
- " " +
811
- d.toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" });
812
-
813
- let badgeHtml = '';
814
- if (run.status === 'running') badgeHtml = '<span class="ahp-run-status running">running</span>';
815
- else if (run.status === 'failed') badgeHtml = '<span class="ahp-run-status failed">failed</span>';
816
- else badgeHtml = '<span class="ahp-run-status completed">done</span>';
817
-
818
- card.innerHTML = `<div class="ahp-run-title">${escapeHtml(run.title || "Untitled")}</div><div class="ahp-run-meta">${badgeHtml}<span>${dateStr}</span></div>`;
819
- card.addEventListener("click", () => {
820
- $$(".ahp-run-card.active").forEach((c) => c.classList.remove("active"));
821
- card.classList.add("active");
822
- loadRunOnCanvas(run.id, run.title, run.status);
823
- });
824
- list.appendChild(card);
1383
+ if (this.socialPulse > 0.02) {
1384
+ ctx.fillStyle = this.hexToRgba("#ec4899", Math.min(0.1, this.socialPulse * 0.08));
1385
+ ctx.fillRect(284, 122, 72, 62);
1386
+ }
1387
+ if (this.errorFlash > 0.02) {
1388
+ ctx.fillStyle = this.hexToRgba(p.error, Math.min(0.1, this.errorFlash * 0.1));
1389
+ ctx.fillRect(0, 0, 384, 216);
825
1390
  }
826
- } catch {
827
- list.innerHTML = '<div class="activity-empty-text">Failed to load</div>';
828
1391
  }
829
- }
830
-
831
- const refreshBtn = $("#activityRefreshBtn");
832
- if (refreshBtn) refreshBtn.addEventListener("click", loadActivityHistory);
833
1392
 
834
- async function loadRunOnCanvas(runId, runTitle, runStatus) {
835
- try {
836
- currentActivityRunId = runId;
837
- const titleEl = $("#activityRunTitle");
838
- if (titleEl) titleEl.textContent = runTitle || `Run ${runId}`;
839
-
840
- // reset badge
841
- const badgeEl = $("#atlRunStatus");
842
- if (badgeEl) {
843
- badgeEl.style.display = "inline-block";
844
- badgeEl.className = "atl-run-badge " + (runStatus === "running" ? "running" : "completed");
845
- badgeEl.textContent = runStatus || "completed";
846
- }
847
-
848
- // reset timer view
849
- stopRunTimer();
850
- const timerEl = $("#atlTimer");
851
- if (timerEl) {
852
- if (runStatus === "running") {
853
- // If it was already running in the background, we might not have a perfect start time,
854
- // but we can just start a fresh timer from now or fetch true duration later.
855
- startRunTimer();
856
- } else {
857
- timerEl.style.display = "none";
858
- }
1393
+ drawScanlines(ctx) {
1394
+ ctx.fillStyle = this.palette.isDark ? "rgba(4, 8, 14, 0.015)" : "rgba(255, 255, 255, 0.015)";
1395
+ for (let y = 0; y < 216; y += 4) {
1396
+ ctx.fillRect(0, y, 384, 1);
859
1397
  }
1398
+ }
860
1399
 
861
- const data = await api(`/agents/${runId}/steps`);
862
- clearActivity();
863
- ensureTimeline();
1400
+ hexToRgba(hex, alpha) {
1401
+ const clean = hex.replace("#", "");
1402
+ const value = parseInt(clean, 16);
1403
+ const r = (value >> 16) & 255;
1404
+ const g = (value >> 8) & 255;
1405
+ const b = value & 255;
1406
+ return `rgba(${r}, ${g}, ${b}, ${alpha})`;
1407
+ }
1408
+ }
864
1409
 
865
- // Explicitly hide empty block just in case
866
- const empty = $("#activityEmpty");
867
- if (empty) empty.style.display = "none";
1410
+ let pixelWorld = null;
868
1411
 
869
- for (const step of data.steps || []) {
870
- let toolInput = {};
871
- let result = null;
872
- try {
873
- toolInput = step.tool_input ? JSON.parse(step.tool_input) : {};
874
- } catch { }
875
- try {
876
- result = step.result ? JSON.parse(step.result) : null;
877
- } catch { }
878
- activityTimeline.addNode(step.id, step.tool_name, toolInput);
879
- activityTimeline.updateNode(
880
- step.id,
881
- step.tool_name,
882
- result,
883
- step.screenshot_path || null,
884
- step.status,
885
- );
886
- }
887
- if (data.response) activityTimeline.addResponse(data.response);
888
- } catch (err) {
889
- toast("Failed to load run", "error");
890
- }
1412
+ function ensureWorld() {
1413
+ if (pixelWorld) return;
1414
+ const canvas = document.getElementById("worldCanvas");
1415
+ if (!canvas) return;
1416
+ pixelWorld = new PixelWorld(canvas);
1417
+ window.pixelWorld = pixelWorld;
891
1418
  }
892
1419
 
1420
+ function resetWorldForNewRun() {
1421
+ ensureWorld();
1422
+ if (pixelWorld) pixelWorld.resetForNewRun();
1423
+ }
1424
+
1425
+ ensureWorld();
1426
+ navigateTo(getPageFromLocation(), { push: false });
1427
+
893
1428
  // ── Socket Events ──
894
1429
 
895
1430
  socket.on("run:start", (data) => {
@@ -900,46 +1435,30 @@ socket.on("run:start", (data) => {
900
1435
  backgroundRunIds.add(data.runId);
901
1436
  return;
902
1437
  }
903
- setTimeout(loadActivityHistory, 100);
904
- currentActivityRunId = data.runId;
905
- const titleEl = $("#activityRunTitle");
906
- if (titleEl) titleEl.textContent = data.title || `Run ${data.runId}`;
907
-
908
- const badgeEl = $("#atlRunStatus");
909
- if (badgeEl) {
910
- badgeEl.style.display = "inline-block";
911
- badgeEl.className = "atl-run-badge running";
912
- badgeEl.textContent = "running";
913
- }
914
-
915
- clearActivity();
916
- ensureTimeline();
917
- startRunTimer();
1438
+ ensureWorld();
1439
+ if (pixelWorld) pixelWorld.onRunStart(data);
918
1440
  });
919
1441
 
920
1442
  socket.on("run:thinking", (data) => {
921
1443
  if (backgroundRunIds.has(data.runId)) return;
922
1444
  const textEl = $("#thinkingText");
923
1445
  if (textEl) textEl.textContent = `Thinking… (step ${data.iteration})`;
1446
+ ensureWorld();
1447
+ if (pixelWorld) pixelWorld.onThinking(data);
924
1448
  });
925
1449
 
926
1450
  socket.on("run:tool_start", (data) => {
927
1451
  if (backgroundRunIds.has(data.runId)) return;
928
- addActivityNode(data.stepId, data.toolName, data.toolArgs, data.runId);
1452
+ ensureWorld();
1453
+ if (pixelWorld) pixelWorld.onToolStart(data);
929
1454
  const textEl = $("#thinkingText");
930
1455
  if (textEl) textEl.textContent = `${data.toolName}…`;
931
1456
  });
932
1457
 
933
1458
  socket.on("run:tool_end", (data) => {
934
1459
  if (backgroundRunIds.has(data.runId)) return;
935
- updateActivityNode(
936
- data.stepId,
937
- data.toolName,
938
- data.result,
939
- data.screenshotPath,
940
- data.status,
941
- data.runId
942
- );
1460
+ ensureWorld();
1461
+ if (pixelWorld) pixelWorld.onToolEnd(data);
943
1462
  });
944
1463
 
945
1464
  socket.on("run:stream", (data) => {
@@ -1007,31 +1526,12 @@ socket.on("run:complete", (data) => {
1007
1526
  appendMessage("assistant", data.content);
1008
1527
  }
1009
1528
 
1010
- addActivityResponse(data.content, data.runId);
1011
-
1012
- if (currentActivityRunId === data.runId) {
1013
- const badgeEl = $("#atlRunStatus");
1014
- if (badgeEl) {
1015
- badgeEl.className = "atl-run-badge " + (data.status || "completed");
1016
- badgeEl.textContent = data.status || "completed";
1017
- }
1018
- stopRunTimer();
1019
-
1020
- // Collapse old steps for cleaner view
1021
- if (activityTimeline) {
1022
- for (const [, info] of activityTimeline.steps) {
1023
- if (!info.isResponse && info.cardEl && info.cardEl.classList.contains("open")) {
1024
- const hadError = info.cardEl.querySelector(".atl-text.error");
1025
- if (!hadError) info.cardEl.classList.remove("open");
1026
- }
1027
- }
1028
- }
1029
- }
1529
+ ensureWorld();
1530
+ if (pixelWorld) pixelWorld.onRunComplete(data);
1030
1531
 
1031
1532
  isStreaming = false;
1032
1533
  sendBtn.disabled = false;
1033
1534
  }
1034
- setTimeout(loadActivityHistory, 100);
1035
1535
  });
1036
1536
 
1037
1537
  socket.on("chat:cleared", () => {
@@ -1044,8 +1544,8 @@ socket.on("run:error", (data) => {
1044
1544
  if (thinking) thinking.remove();
1045
1545
  const errMsg = data.error || "Unknown error";
1046
1546
  appendMessage("assistant", `❌ ${errMsg}`);
1047
- const badge = $("#activityBadge");
1048
- if (badge) badge.classList.remove("hidden");
1547
+ ensureWorld();
1548
+ if (pixelWorld) pixelWorld.onRunError(data);
1049
1549
  isStreaming = false;
1050
1550
  sendBtn.disabled = false;
1051
1551
  toast(errMsg, "error");
@@ -1056,27 +1556,22 @@ socket.on("run:interim", (data) => {
1056
1556
  const textEl = $("#thinkingText");
1057
1557
  if (textEl) textEl.textContent = data.message;
1058
1558
  appendInterimMessage(data.message);
1559
+ ensureWorld();
1560
+ if (pixelWorld) pixelWorld.pushEvent("note", data.message);
1059
1561
  });
1060
1562
 
1061
- // Incoming social message → show in chat + activity canvas
1563
+ // Incoming social message → show in chat + world visualization
1062
1564
  socket.on("messaging:message", (data) => {
1063
1565
  appendSocialMessage(data.platform, "user", data.content, data.senderName);
1064
- ensureTimeline();
1065
- const stepId = `msg-${Date.now()}`;
1066
- activityTimeline.addNode(stepId, "send_message", {
1067
- platform: data.platform,
1068
- to: data.chatId,
1069
- content: data.content,
1070
- });
1071
- activityTimeline.updateNode(
1072
- stepId,
1073
- "send_message",
1074
- { received: true, from: data.senderName },
1075
- null,
1076
- "completed",
1077
- );
1078
- const badge = $("#activityBadge");
1079
- if (badge) badge.classList.remove("hidden");
1566
+ ensureWorld();
1567
+ if (pixelWorld) pixelWorld.onMessage(data);
1568
+ });
1569
+
1570
+ socket.on("skill:draft_created", (data) => {
1571
+ toast(`Draft skill created: ${data.name}`, "success");
1572
+ if (!$("#skillList")?.classList.contains("hidden")) {
1573
+ loadSkillsPage();
1574
+ }
1080
1575
  });
1081
1576
 
1082
1577
  // ── Logs Tab ──
@@ -1158,12 +1653,12 @@ if (copyLogsBtn) {
1158
1653
  debugText += `[${sender}]\\n${content.trim()}\\n\\n`;
1159
1654
  });
1160
1655
 
1161
- debugText += "--- ACTIVITY TIMELINE ---\\n";
1162
- const nodes = document.querySelectorAll(".node-view");
1163
- nodes.forEach((n) => {
1164
- const title = n.querySelector(".node-title")?.innerText || "Node";
1656
+ debugText += "--- WORLD EVENT FEED ---\\n";
1657
+ const entries = document.querySelectorAll(".world-event-entry");
1658
+ entries.forEach((entry) => {
1659
+ const title = entry.querySelector(".world-event-tag")?.innerText || "EVENT";
1165
1660
  const details =
1166
- n.querySelector(".node-details")?.innerText || "No details";
1661
+ entry.querySelector(".world-event-text")?.innerText || "No details";
1167
1662
  debugText += `${title}\\n${details}\\n\\n`;
1168
1663
  });
1169
1664
 
@@ -1328,7 +1823,11 @@ $("#settingsBtn").addEventListener("click", async () => {
1328
1823
 
1329
1824
  try {
1330
1825
  const backendVersion = await api("/version");
1331
- backendVersionLabel = `${backendVersion?.version || "unknown"}${backendVersion?.gitSha ? ` (${backendVersion.gitSha})` : ""}`;
1826
+ backendVersionLabel = backendVersion?.version || "unknown";
1827
+ const vEl = $("#settingsAppVersion");
1828
+ if (vEl && backendVersionLabel !== "unknown") {
1829
+ vEl.textContent = `v${backendVersionLabel}`;
1830
+ }
1332
1831
  } catch {
1333
1832
  backendVersionLabel = "unavailable";
1334
1833
  }
@@ -1347,6 +1846,9 @@ $("#settingsBtn").addEventListener("click", async () => {
1347
1846
  $("#settingHeadlessBrowser").checked =
1348
1847
  settings.headless_browser !== false &&
1349
1848
  settings.headless_browser !== "false";
1849
+ $("#settingAutoSkillLearning").checked =
1850
+ settings.auto_skill_learning !== false &&
1851
+ settings.auto_skill_learning !== "false";
1350
1852
 
1351
1853
  const enabledModels = Array.isArray(settings.enabled_models) ? settings.enabled_models : (meta.models || []).map(m => m.id);
1352
1854
 
@@ -1396,6 +1898,8 @@ $("#settingsBtn").addEventListener("click", async () => {
1396
1898
  checkbox.type = "checkbox";
1397
1899
  checkbox.className = "dynamic-model-checkbox";
1398
1900
  checkbox.dataset.modelId = modelDef.id;
1901
+ checkbox.autocomplete = "off";
1902
+ checkbox.setAttribute("data-bwignore", "true");
1399
1903
  checkbox.checked = enabledModels.includes(modelDef.id);
1400
1904
 
1401
1905
  const span = document.createElement("span");
@@ -1442,6 +1946,7 @@ $("#saveSettings").addEventListener("click", async () => {
1442
1946
  body: {
1443
1947
  heartbeat_enabled: $("#settingHeartbeat").checked,
1444
1948
  headless_browser: $("#settingHeadlessBrowser").checked,
1949
+ auto_skill_learning: $("#settingAutoSkillLearning").checked,
1445
1950
  enabled_models: enabledModels,
1446
1951
  default_chat_model: defaultChatModel,
1447
1952
  default_subagent_model: defaultSubagentModel
@@ -1561,11 +2066,76 @@ async function loadMemoryPage() {
1561
2066
 
1562
2067
  // Memories list
1563
2068
  await _loadMemoriesTab(_memActiveCategory);
2069
+ await loadSessionRecall();
1564
2070
  } catch (err) {
1565
2071
  toast("Failed to load memory", "error");
1566
2072
  }
1567
2073
  }
1568
2074
 
2075
+ async function loadSessionRecall(query = "") {
2076
+ const container = $("#sessionRecallList");
2077
+ if (!container) return;
2078
+ container.innerHTML =
2079
+ '<div class="empty-state"><p>Loading session recall…</p></div>';
2080
+ try {
2081
+ const results = query
2082
+ ? await api("/memory/conversations/search", {
2083
+ method: "POST",
2084
+ body: { query, limit: 8 },
2085
+ })
2086
+ : await api("/memory/conversations?limit=12");
2087
+
2088
+ if (!results.length) {
2089
+ container.innerHTML =
2090
+ '<div class="empty-state"><p>No matching sessions yet.</p></div>';
2091
+ return;
2092
+ }
2093
+
2094
+ container.innerHTML = "";
2095
+ for (const item of results) {
2096
+ const card = document.createElement("div");
2097
+ card.className = "item-card";
2098
+
2099
+ if (item.matches) {
2100
+ const matches = item.matches
2101
+ .map(
2102
+ (match) => `<div style="margin-top:8px;padding:8px 10px;border:1px solid var(--border);border-radius:10px;">
2103
+ <div style="font-size:0.72rem;text-transform:uppercase;color:var(--text-muted);margin-bottom:4px;">${escapeHtml(match.role || "message")}</div>
2104
+ <div style="font-size:0.9rem;line-height:1.45;">${escapeHtml(match.excerpt || "")}</div>
2105
+ </div>`
2106
+ )
2107
+ .join("");
2108
+
2109
+ card.innerHTML = `
2110
+ <div class="item-card-header">
2111
+ <div>
2112
+ <div class="item-card-title">${escapeHtml(item.title || "Session")}</div>
2113
+ <div class="item-card-meta">${escapeHtml(item.source || "session")} · ${escapeHtml(item.createdAt || "")}</div>
2114
+ </div>
2115
+ <span class="badge badge-neutral">${item.matchCount || item.matches.length} match${(item.matchCount || item.matches.length) === 1 ? "" : "es"}</span>
2116
+ </div>
2117
+ ${matches}
2118
+ `;
2119
+ } else {
2120
+ card.innerHTML = `
2121
+ <div class="item-card-header">
2122
+ <div>
2123
+ <div class="item-card-title">${escapeHtml(item.title || "Session")}</div>
2124
+ <div class="item-card-meta">${escapeHtml(item.status || "completed")} · ${escapeHtml(item.completedAt || item.createdAt || "")}</div>
2125
+ </div>
2126
+ </div>
2127
+ <div style="font-size:0.9rem;line-height:1.45;color:var(--text);">${escapeHtml(item.excerpt || "No excerpt available.")}</div>
2128
+ `;
2129
+ }
2130
+
2131
+ container.appendChild(card);
2132
+ }
2133
+ } catch {
2134
+ container.innerHTML =
2135
+ '<div class="empty-state"><p>Session recall failed to load.</p></div>';
2136
+ }
2137
+ }
2138
+
1569
2139
  async function _loadMemoriesTab(category = "") {
1570
2140
  const container = $("#memoryList");
1571
2141
  if (!container) return;
@@ -1700,6 +2270,15 @@ $("#memorySearchInput")?.addEventListener("keydown", (e) => {
1700
2270
  if (e.key === "Enter") $("#memorySearchBtn")?.click();
1701
2271
  });
1702
2272
 
2273
+ $("#sessionSearchBtn")?.addEventListener("click", async () => {
2274
+ const query = $("#sessionSearchInput")?.value?.trim() || "";
2275
+ await loadSessionRecall(query);
2276
+ });
2277
+
2278
+ $("#sessionSearchInput")?.addEventListener("keydown", (e) => {
2279
+ if (e.key === "Enter") $("#sessionSearchBtn")?.click();
2280
+ });
2281
+
1703
2282
  // Soul save
1704
2283
  $("#saveSoulBtn")?.addEventListener("click", async () => {
1705
2284
  try {
@@ -1850,8 +2429,16 @@ document.querySelectorAll("[data-skills-tab]").forEach((tab) => {
1850
2429
  });
1851
2430
  });
1852
2431
 
1853
- async function loadSkillStore() {
2432
+ async function loadSkillStore(options = {}) {
1854
2433
  const wrap = $("#skillStore");
2434
+ const pageBody = wrap.closest(".page-body");
2435
+ const shouldPreserveState = !!options.preserveState;
2436
+ const previousState = {
2437
+ filter: wrap.dataset.storeFilter || "",
2438
+ pageScrollTop: pageBody ? pageBody.scrollTop : 0,
2439
+ panelScrollTop: wrap.scrollTop || 0,
2440
+ };
2441
+
1855
2442
  wrap.innerHTML = '<div class="empty-state"><p>Loading store…</p></div>';
1856
2443
  try {
1857
2444
  const items = await api("/store");
@@ -1882,6 +2469,7 @@ async function loadSkillStore() {
1882
2469
  searchInp.type = "text";
1883
2470
  searchInp.className = "input";
1884
2471
  searchInp.placeholder = "Search skills…";
2472
+ searchInp.value = previousState.filter;
1885
2473
  searchRow.appendChild(searchInp);
1886
2474
  wrap.appendChild(searchRow);
1887
2475
 
@@ -1939,11 +2527,12 @@ async function loadSkillStore() {
1939
2527
  '<div class="empty-state"><p>No matching skills</p></div>';
1940
2528
  }
1941
2529
 
1942
- renderStore("");
2530
+ renderStore(previousState.filter.trim().toLowerCase());
1943
2531
 
1944
- searchInp.addEventListener("input", () =>
1945
- renderStore(searchInp.value.trim().toLowerCase()),
1946
- );
2532
+ searchInp.addEventListener("input", () => {
2533
+ wrap.dataset.storeFilter = searchInp.value;
2534
+ renderStore(searchInp.value.trim().toLowerCase());
2535
+ });
1947
2536
 
1948
2537
  cardsWrap.addEventListener("click", async (e) => {
1949
2538
  const btn = e.target.closest("[data-store-action]");
@@ -1959,12 +2548,19 @@ async function loadSkillStore() {
1959
2548
  await api(`/store/${storeId}/uninstall`, { method: "DELETE" });
1960
2549
  toast("Skill removed", "info");
1961
2550
  }
1962
- await loadSkillStore(); // refresh
2551
+ await loadSkillStore({ preserveState: true }); // refresh without jumping back to top
1963
2552
  } catch (err) {
1964
2553
  toast("Error: " + err.message, "error");
1965
2554
  btn.disabled = false;
1966
2555
  }
1967
2556
  });
2557
+
2558
+ if (shouldPreserveState) {
2559
+ requestAnimationFrame(() => {
2560
+ if (pageBody) pageBody.scrollTop = previousState.pageScrollTop;
2561
+ wrap.scrollTop = previousState.panelScrollTop;
2562
+ });
2563
+ }
1968
2564
  } catch (err) {
1969
2565
  wrap.innerHTML =
1970
2566
  '<div class="empty-state"><p>Failed to load store</p></div>';
@@ -1991,6 +2587,11 @@ async function loadSkillsPage() {
1991
2587
  for (const skill of skills) {
1992
2588
  const card = document.createElement("div");
1993
2589
  card.className = "item-card";
2590
+ const badges = [
2591
+ `<span class="badge ${skill.enabled ? "badge-success" : "badge-neutral"}">${skill.enabled ? "Active" : "Disabled"}</span>`,
2592
+ ];
2593
+ if (skill.draft) badges.push('<span class="badge badge-warning">Draft</span>');
2594
+ if (skill.autoCreated) badges.push('<span class="badge badge-info">Auto-learned</span>');
1994
2595
  card.innerHTML = `
1995
2596
  <div class="item-card-header">
1996
2597
  <div>
@@ -1998,12 +2599,14 @@ async function loadSkillsPage() {
1998
2599
  <div class="item-card-meta">${escapeHtml(skill.description)}</div>
1999
2600
  </div>
2000
2601
  <div class="item-card-actions">
2001
- <span class="badge ${skill.enabled ? "badge-success" : "badge-neutral"}">${skill.enabled ? "Active" : "Disabled"}</span>
2002
- <button class="btn btn-sm btn-secondary" data-action="editSkill" data-filename="${escapeHtml(skill.filename)}">Edit</button>
2003
- <button class="btn btn-sm btn-danger" data-action="deleteSkill" data-filename="${escapeHtml(skill.filename)}">&times;</button>
2602
+ ${badges.join("")}
2603
+ <button class="btn btn-sm btn-secondary" data-action="toggleSkill" data-name="${escapeHtml(skill.name)}" data-enabled="${skill.enabled ? "true" : "false"}">${skill.enabled ? "Disable" : "Enable"}</button>
2604
+ <button class="btn btn-sm btn-secondary" data-action="editSkill" data-name="${escapeHtml(skill.name)}">Edit</button>
2605
+ <button class="btn btn-sm btn-danger" data-action="deleteSkill" data-name="${escapeHtml(skill.name)}">&times;</button>
2004
2606
  </div>
2005
2607
  </div>
2006
- <div class="item-card-meta">Trigger: ${escapeHtml(skill.trigger || "N/A")} | Category: ${escapeHtml(skill.category)}</div>
2608
+ <div class="item-card-meta">Trigger: ${escapeHtml(skill.trigger || "N/A")} | Category: ${escapeHtml(skill.category)} | Source: ${escapeHtml(skill.source || "local")}</div>
2609
+ <div class="item-card-meta" style="margin-top:6px;">${escapeHtml(skill.filePath || "")}</div>
2007
2610
  `;
2008
2611
  container.appendChild(card);
2009
2612
  }
@@ -2012,12 +2615,12 @@ async function loadSkillsPage() {
2012
2615
  }
2013
2616
  }
2014
2617
 
2015
- window.editSkill = async (filename) => {
2618
+ window.editSkill = async (name) => {
2016
2619
  try {
2017
- const data = await api(`/skills/${filename}`);
2620
+ const data = await api(`/skills/${name}`);
2018
2621
  const content = prompt("Edit skill content:", data.content);
2019
2622
  if (content !== null) {
2020
- await api(`/skills/${filename}`, { method: "PUT", body: { content } });
2623
+ await api(`/skills/${name}`, { method: "PUT", body: { content } });
2021
2624
  loadSkillsPage();
2022
2625
  toast("Skill updated", "success");
2023
2626
  }
@@ -2026,10 +2629,10 @@ window.editSkill = async (filename) => {
2026
2629
  }
2027
2630
  };
2028
2631
 
2029
- window.deleteSkill = async (filename) => {
2030
- if (!confirm(`Delete skill ${filename}?`)) return;
2632
+ window.deleteSkill = async (name) => {
2633
+ if (!confirm(`Delete skill ${name}?`)) return;
2031
2634
  try {
2032
- await api(`/skills/${filename}`, { method: "DELETE" });
2635
+ await api(`/skills/${name}`, { method: "DELETE" });
2033
2636
  loadSkillsPage();
2034
2637
  toast("Skill deleted", "success");
2035
2638
  } catch (err) {
@@ -2037,13 +2640,28 @@ window.deleteSkill = async (filename) => {
2037
2640
  }
2038
2641
  };
2039
2642
 
2643
+ window.toggleSkill = async (name, enabled) => {
2644
+ try {
2645
+ await api(`/skills/${name}`, {
2646
+ method: "PUT",
2647
+ body: { enabled },
2648
+ });
2649
+ loadSkillsPage();
2650
+ toast(enabled ? "Skill enabled" : "Skill disabled", "success");
2651
+ } catch (err) {
2652
+ toast("Failed to update skill", "error");
2653
+ }
2654
+ };
2655
+
2040
2656
  // Skills event delegation
2041
2657
  $("#skillList").addEventListener("click", (e) => {
2042
2658
  const btn = e.target.closest("[data-action]");
2043
2659
  if (!btn) return;
2044
2660
  const action = btn.dataset.action;
2045
- if (action === "editSkill") window.editSkill(btn.dataset.filename);
2046
- else if (action === "deleteSkill") window.deleteSkill(btn.dataset.filename);
2661
+ if (action === "editSkill") window.editSkill(btn.dataset.name);
2662
+ else if (action === "deleteSkill") window.deleteSkill(btn.dataset.name);
2663
+ else if (action === "toggleSkill")
2664
+ window.toggleSkill(btn.dataset.name, btn.dataset.enabled !== "true");
2047
2665
  });
2048
2666
 
2049
2667
  $("#addSkillBtn").addEventListener("click", () => {