@agentprojectcontext/apx 1.55.1 → 1.56.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -18,8 +18,8 @@
18
18
  <link rel="apple-touch-icon" href="/favicon/dark/apple-touch-icon.png" media="(prefers-color-scheme: dark)" />
19
19
  <link rel="manifest" href="/favicon/white/site.webmanifest" media="(prefers-color-scheme: light)" />
20
20
  <link rel="manifest" href="/favicon/dark/site.webmanifest" media="(prefers-color-scheme: dark)" />
21
- <script type="module" crossorigin src="/assets/index-BxS9xYHB.js"></script>
22
- <link rel="stylesheet" crossorigin href="/assets/index-BwnMDZaD.css">
21
+ <script type="module" crossorigin src="/assets/index-nL0K1Vt_.js"></script>
22
+ <link rel="stylesheet" crossorigin href="/assets/index-CAUezTBY.css">
23
23
  </head>
24
24
  <body class="bg-background text-foreground antialiased">
25
25
  <div id="root"></div>
@@ -235,6 +235,7 @@ function projectLabel(key?: string) {
235
235
  case "routines": return t("project.nav.routines");
236
236
  case "tasks": return t("project.nav.tasks");
237
237
  case "mcps": return t("project.nav.mcps");
238
+ case "artifacts": return t("project.nav.artifacts");
238
239
  case "config": return t("project.nav.config");
239
240
  case "workspaces": return t("base.workspaces_title");
240
241
  case "models": return t("settings.tabs.engines");
@@ -126,10 +126,13 @@ export function ChatList({
126
126
  const [collapsed, setCollapsed] = useState<Partial<Record<ChannelGroupKey, boolean>>>({});
127
127
  const [byAgent, setByAgent] = useState<Record<string, ConversationListEntry[]>>({});
128
128
 
129
- // Super-agent channel threads (telegram, web quick-chat, desktop …) from the
130
- // global message ledger the chats that don't live in conversation files.
129
+ // Super-agent channel threads (telegram, web quick-chat, desktop …) come from
130
+ // the global message ledger, which is daemon-level and NOT project-scoped.
131
+ // Only surface them in the Base workspace (pid "0"); inside a real project the
132
+ // sidebar shows just that project's own agent conversations.
133
+ const isBase = String(pid) === "0";
131
134
  const threadsQ = useSWR(
132
- `/projects/${pid}/super-agent/threads`,
135
+ isBase ? `/projects/${pid}/super-agent/threads` : null,
133
136
  () => Conversations.threads(pid),
134
137
  { revalidateOnFocus: false },
135
138
  );
@@ -197,27 +197,36 @@ function ArtifactRow({
197
197
  </DialogContent>
198
198
  </Dialog>
199
199
 
200
- {/* Editar — opens as a file tab in the main panel */}
201
- <Tip content={t("code_module.artifacts_edit")}>
202
- <button
203
- type="button"
204
- onClick={() => onEditArtifact?.(entry.name)}
205
- className="inline-flex items-center gap-1 rounded px-1.5 py-1 text-[10px] font-medium bg-violet-500/15 text-violet-700 hover:bg-violet-500/25 dark:text-violet-300"
206
- >
207
- <SquarePen className="size-3" />
208
- {t("modules_ui.code_artifact_edit_short")}
209
- </button>
210
- </Tip>
200
+ {/* Editar — opens as a file tab in the main panel. Only available when
201
+ a host editor (the Code screen) is wired in. */}
202
+ {onEditArtifact && (
203
+ <Tip content={t("code_module.artifacts_edit")}>
204
+ <button
205
+ type="button"
206
+ onClick={() => onEditArtifact(entry.name)}
207
+ className="inline-flex items-center gap-1 rounded px-1.5 py-1 text-[10px] font-medium bg-violet-500/15 text-violet-700 hover:bg-violet-500/25 dark:text-violet-300"
208
+ >
209
+ <SquarePen className="size-3" />
210
+ {t("modules_ui.code_artifact_edit_short")}
211
+ </button>
212
+ </Tip>
213
+ )}
211
214
 
212
- {/* Run button */}
215
+ {/* Run button — hand off to the terminal when there is one (Code
216
+ screen), otherwise execute in place and show the captured output. */}
213
217
  {looksRunnable && (
214
218
  <Tip content={t("code_module.artifacts_run")}>
215
219
  <button
216
220
  type="button"
217
- onClick={() => onRunInTerminal?.(`apx artifact run ${entry.name}`)}
218
- className="inline-flex items-center gap-1 rounded px-1.5 py-1 text-[10px] font-medium bg-emerald-500/15 text-emerald-700 hover:bg-emerald-500/25 dark:text-emerald-300"
221
+ disabled={running}
222
+ onClick={() =>
223
+ onRunInTerminal
224
+ ? onRunInTerminal(`apx artifact run ${entry.name}`)
225
+ : void run()
226
+ }
227
+ className="inline-flex items-center gap-1 rounded px-1.5 py-1 text-[10px] font-medium bg-emerald-500/15 text-emerald-700 hover:bg-emerald-500/25 disabled:opacity-60 dark:text-emerald-300"
219
228
  >
220
- <Play className="size-3" />
229
+ {running ? <Spinner size={10} /> : <Play className="size-3" />}
221
230
  {t("code_module.artifacts_run")}
222
231
  </button>
223
232
  </Tip>
@@ -250,6 +250,11 @@ export function useChat(pid: string, onError?: (msg: string) => void): UseChatRe
250
250
  const [conversationId, setConversationId] = useState<string | undefined>(undefined);
251
251
  const abortRef = useRef<AbortController | null>(null);
252
252
  const convoRef = useRef<string | undefined>(undefined);
253
+ // Monotonic token guarding async history loads. Every load()/loadThread()/
254
+ // clear() bumps it; a load only applies its result if it's still the latest.
255
+ // Without this, clicking chat A then B could land A's (slower) response last
256
+ // and paint A's messages under B's header.
257
+ const loadSeqRef = useRef(0);
253
258
 
254
259
  // Mutate the trailing assistant turn in place.
255
260
  const patchLast = useCallback((fn: (m: ChatMsg) => ChatMsg) => {
@@ -348,6 +353,7 @@ export function useChat(pid: string, onError?: (msg: string) => void): UseChatRe
348
353
  const stop = useCallback(() => abortRef.current?.abort(), []);
349
354
  const clear = useCallback(() => {
350
355
  if (streaming) return;
356
+ loadSeqRef.current++; // cancel any in-flight history load
351
357
  convoRef.current = undefined;
352
358
  setConversationId(undefined);
353
359
  setMsgs([]);
@@ -356,8 +362,13 @@ export function useChat(pid: string, onError?: (msg: string) => void): UseChatRe
356
362
  const load = useCallback(
357
363
  async (agentSlug: string, conversationId: string) => {
358
364
  if (streaming) return;
365
+ const seq = ++loadSeqRef.current;
366
+ // Blank the pane up front so it never shows the previous chat under the
367
+ // new header while the fetch is in flight.
368
+ setMsgs([]);
359
369
  try {
360
370
  const detail = await Conversations.get(pid, agentSlug, conversationId);
371
+ if (seq !== loadSeqRef.current) return; // superseded by a newer pick
361
372
  const loaded: ChatMsg[] = (detail.messages ?? [])
362
373
  .filter((m) => m.role === "user" || m.role === "assistant")
363
374
  .map((m) => ({
@@ -369,6 +380,10 @@ export function useChat(pid: string, onError?: (msg: string) => void): UseChatRe
369
380
  setConversationId(conversationId);
370
381
  setMsgs(loaded);
371
382
  } catch (e) {
383
+ if (seq !== loadSeqRef.current) return;
384
+ convoRef.current = undefined;
385
+ setConversationId(undefined);
386
+ setMsgs([]);
372
387
  onError?.((e as Error)?.message || t("shared_ui.err_load_conversation"));
373
388
  }
374
389
  },
@@ -378,8 +393,11 @@ export function useChat(pid: string, onError?: (msg: string) => void): UseChatRe
378
393
  const loadThread = useCallback(
379
394
  async (channel: string, threadId: string) => {
380
395
  if (streaming) return;
396
+ const seq = ++loadSeqRef.current;
397
+ setMsgs([]);
381
398
  try {
382
399
  const detail = await Conversations.thread(pid, channel, threadId);
400
+ if (seq !== loadSeqRef.current) return; // superseded by a newer pick
383
401
  const loaded: ChatMsg[] = (detail.messages ?? [])
384
402
  .filter((m) => m.role === "user" || m.role === "assistant")
385
403
  .map((m) => ({
@@ -393,6 +411,10 @@ export function useChat(pid: string, onError?: (msg: string) => void): UseChatRe
393
411
  setConversationId(undefined);
394
412
  setMsgs(loaded);
395
413
  } catch (e) {
414
+ if (seq !== loadSeqRef.current) return;
415
+ convoRef.current = undefined;
416
+ setConversationId(undefined);
417
+ setMsgs([]);
396
418
  onError?.((e as Error)?.message || t("shared_ui.err_load_conversation"));
397
419
  }
398
420
  },
@@ -288,6 +288,7 @@ export const en = {
288
288
  routines: "Routines",
289
289
  tasks: "Tasks",
290
290
  mcps: "MCPs",
291
+ artifacts: "Artifacts",
291
292
  vars: "Variables",
292
293
  logs: "Logs",
293
294
  memories: "Memories",
@@ -305,10 +306,16 @@ export const en = {
305
306
  routines: "Routines",
306
307
  agents: "Agents",
307
308
  mcps: "MCPs",
309
+ artifacts: "Artifacts",
308
310
  chat: "Chat (super-agent)",
309
311
  chat_value: "open",
310
312
  },
311
313
 
314
+ artifacts: {
315
+ title: "Artifacts",
316
+ subtitle: "Reusable scripts and files stored under the project. Agents create them; you can view, run, rename or delete them.",
317
+ },
318
+
312
319
  chat: {
313
320
  title: "Chat with agent",
314
321
  subtitle: "Direct conversations with project agents. The super-agent does not intervene.",
@@ -289,6 +289,7 @@ export const es = {
289
289
  routines: "Rutinas",
290
290
  tasks: "Tasks",
291
291
  mcps: "MCPs",
292
+ artifacts: "Artifacts",
292
293
  vars: "Variables",
293
294
  logs: "Logs",
294
295
  memories: "Memorias",
@@ -306,10 +307,16 @@ export const es = {
306
307
  routines: "Rutinas",
307
308
  agents: "Agents",
308
309
  mcps: "MCPs",
310
+ artifacts: "Artifacts",
309
311
  chat: "Chat (super-agent)",
310
312
  chat_value: "abrir",
311
313
  },
312
314
 
315
+ artifacts: {
316
+ title: "Artifacts",
317
+ subtitle: "Scripts y archivos reutilizables guardados en el proyecto. Los crean los agentes; podés verlos, ejecutarlos, renombrarlos o eliminarlos.",
318
+ },
319
+
313
320
  chat: {
314
321
  title: "Chat con agente",
315
322
  subtitle: "Chat directo con el agente del proyecto.",
@@ -3,7 +3,7 @@ import { useParams, Routes, Route, Navigate, useLocation, useNavigate } from "re
3
3
  import {
4
4
  Bot, Heart, Zap, Puzzle, FolderKanban, Settings,
5
5
  MessagesSquare, Send, KeyRound,
6
- LayoutDashboard, Boxes, Cpu, ScrollText, History, Brain,
6
+ LayoutDashboard, Boxes, Cpu, ScrollText, History, Brain, FileCode2,
7
7
  } from "lucide-react";
8
8
  import { useNavCollapse, type TabSection } from "../components/common/TabNav";
9
9
  import { TabLayout } from "../components/common/TabLayout";
@@ -28,11 +28,12 @@ import { VarsTab } from "./project/VarsTab";
28
28
  import { ChatTab } from "./project/ChatTab";
29
29
  import { TelegramTab } from "./project/TelegramTab";
30
30
  import { MemoriesTab } from "./project/MemoriesTab";
31
+ import { ArtifactsTab } from "./project/ArtifactsTab";
31
32
  import { AgentDetailScreen } from "./project/AgentDetailScreen";
32
33
 
33
34
  type NavKey =
34
35
  | "" | "chat" | "config" | "telegram"
35
- | "agents" | "routines" | "tasks" | "mcps" | "vars" | "logs" | "memories";
36
+ | "agents" | "routines" | "tasks" | "mcps" | "vars" | "logs" | "memories" | "artifacts";
36
37
 
37
38
  export function ProjectScreen() {
38
39
  const navigate = useNavigate();
@@ -69,10 +70,11 @@ export function ProjectScreen() {
69
70
  items: [
70
71
  { key: "agents", label: t("project.nav.agents"), icon: Bot },
71
72
  { key: "memories", label: t("project.nav.memories"), icon: Brain },
72
- { key: "routines", label: t("project.nav.routines"), icon: Heart },
73
- { key: "mcps", label: t("project.nav.mcps"), icon: Puzzle },
74
- { key: "vars", label: t("project.nav.vars"), icon: KeyRound },
75
- { key: "config", label: t("project.nav.config"), icon: Settings },
73
+ { key: "routines", label: t("project.nav.routines"), icon: Heart },
74
+ { key: "mcps", label: t("project.nav.mcps"), icon: Puzzle },
75
+ { key: "artifacts", label: t("project.nav.artifacts"), icon: FileCode2 },
76
+ { key: "vars", label: t("project.nav.vars"), icon: KeyRound },
77
+ { key: "config", label: t("project.nav.config"), icon: Settings },
76
78
  ],
77
79
  },
78
80
  ];
@@ -91,11 +93,12 @@ export function ProjectScreen() {
91
93
  {
92
94
  title: t("project.sections.automation"),
93
95
  items: [
94
- { key: "routines", label: t("project.nav.routines"), icon: Heart },
95
- { key: "tasks", label: t("project.nav.tasks"), icon: Zap },
96
- { key: "mcps", label: t("project.nav.mcps"), icon: Puzzle },
97
- { key: "vars", label: t("project.nav.vars"), icon: KeyRound },
98
- { key: "logs", label: t("project.nav.logs"), icon: ScrollText },
96
+ { key: "routines", label: t("project.nav.routines"), icon: Heart },
97
+ { key: "tasks", label: t("project.nav.tasks"), icon: Zap },
98
+ { key: "mcps", label: t("project.nav.mcps"), icon: Puzzle },
99
+ { key: "artifacts", label: t("project.nav.artifacts"), icon: FileCode2 },
100
+ { key: "vars", label: t("project.nav.vars"), icon: KeyRound },
101
+ { key: "logs", label: t("project.nav.logs"), icon: ScrollText },
99
102
  ],
100
103
  },
101
104
  {
@@ -156,6 +159,7 @@ export function ProjectScreen() {
156
159
  <Route path="routines" element={<RoutinesTab pid={pid} />} />
157
160
  <Route path="tasks" element={isBase ? <GlobalTasksTab /> : <TasksTab pid={pid} />} />
158
161
  <Route path="mcps" element={<McpsTab pid={pid} />} />
162
+ <Route path="artifacts" element={<ArtifactsTab pid={pid} />} />
159
163
  <Route path="vars" element={<VarsTab pid={pid} />} />
160
164
  <Route path="threads" element={<Navigate to={`/p/${pid}/chat`} replace />} />
161
165
  <Route path="chat" element={<ChatTab pid={pid} />} />
@@ -1,4 +1,5 @@
1
1
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
2
+ import { useSearchParams } from "react-router-dom";
2
3
  import useSWR from "swr";
3
4
  import { Bot, FolderTree, MessageSquare, PanelLeft, PanelRight, Terminal, X } from "lucide-react";
4
5
  import { Group as PanelGroup, Panel, Separator as PanelResizeHandle } from "react-resizable-panels";
@@ -64,6 +65,8 @@ export function CodeScreen() {
64
65
  const [termInitCmd, setTermInitCmd] = useState("");
65
66
  const [worktreeOpen, setWorktreeOpen] = useState(false);
66
67
  const abortRef = useRef<AbortController | null>(null);
68
+ const [searchParams, setSearchParams] = useSearchParams();
69
+ const deepLinkDone = useRef(false);
67
70
 
68
71
  // Open file tabs. `artifactName` marks an artifact opened for editing;
69
72
  // saves route through Artifacts.write instead of being read-only.
@@ -346,6 +349,27 @@ export function CodeScreen() {
346
349
  [pid],
347
350
  );
348
351
 
352
+ // Deep-link from the project Artifacts tab (/m/code?pid=..&cmd=.. or &edit=..).
353
+ // Select the requested project, then prefill the terminal with the artifact
354
+ // command (so args like a URL can be typed) or open the file for editing.
355
+ useEffect(() => {
356
+ if (deepLinkDone.current) return;
357
+ const wantPid = searchParams.get("pid");
358
+ const cmd = searchParams.get("cmd");
359
+ const edit = searchParams.get("edit");
360
+ if (!wantPid || (!cmd && !edit)) return;
361
+ // Wait until the requested project is active so the command/edit targets it.
362
+ if (String(pid) !== String(wantPid)) {
363
+ setPid(String(wantPid));
364
+ return;
365
+ }
366
+ deepLinkDone.current = true;
367
+ if (edit) openArtifact(edit);
368
+ if (cmd) runInTerminal(cmd.endsWith(" ") ? cmd : cmd + " ");
369
+ // Clear params so a refresh/back doesn't retrigger the handoff.
370
+ setSearchParams({}, { replace: true });
371
+ }, [searchParams, pid, openArtifact, runInTerminal, setSearchParams]);
372
+
349
373
  const saveOpenFile = useCallback(
350
374
  async (path: string, content: string) => {
351
375
  const file = openFiles.find((f) => f.path === path);
@@ -0,0 +1,32 @@
1
+ import { useNavigate } from "react-router-dom";
2
+ import { Section } from "../../components/Section";
3
+ import { CodeArtifactsTab } from "../../components/code/CodeArtifactsTab";
4
+ import { t } from "../../i18n";
5
+
6
+ // Project-level view of the artifacts stored under <project>/artifacts/. Reuses
7
+ // the same list/row UI as the Code screen. Run and Edit hand off to the Code
8
+ // module — the terminal there lets you pass args (e.g. a URL) and the file
9
+ // editor lets you edit — instead of running headless in place.
10
+ export function ArtifactsTab({ pid }: { pid: string }) {
11
+ const navigate = useNavigate();
12
+
13
+ const toCode = (params: Record<string, string>) => {
14
+ const qs = new URLSearchParams({ pid, ...params }).toString();
15
+ navigate(`/m/code?${qs}`);
16
+ };
17
+
18
+ return (
19
+ <Section
20
+ title={t("project.artifacts.title")}
21
+ description={t("project.artifacts.subtitle")}
22
+ fullHeight
23
+ className="min-h-[24rem]"
24
+ >
25
+ <CodeArtifactsTab
26
+ pid={pid}
27
+ onRunInTerminal={(cmd) => toCode({ cmd })}
28
+ onEditArtifact={(name) => toCode({ edit: name })}
29
+ />
30
+ </Section>
31
+ );
32
+ }
@@ -65,8 +65,11 @@ export function ChatTab({ pid }: { pid: string }) {
65
65
  } else if (selected.kind === "thread") {
66
66
  void loadThread(selected.channel, selected.threadId);
67
67
  } else {
68
- // Switching to a live session = drop any previously bound conversation.
69
- if (conversationId) clear();
68
+ // Live session selected always start from a clean slate. (Threads leave
69
+ // conversationId undefined, so an `if (conversationId)` guard would skip
70
+ // clearing and the previous chat's messages would linger under the new
71
+ // header — the "title changes but content stays" bug.)
72
+ clear();
70
73
  }
71
74
  // eslint-disable-next-line react-hooks/exhaustive-deps
72
75
  }, [
@@ -1,7 +1,7 @@
1
1
  import useSWR from "swr";
2
2
  import { NavLink } from "react-router-dom";
3
- import { Bot, Heart, MessagesSquare, Puzzle, Zap } from "lucide-react";
4
- import { Agents, Mcps, Routines, Tasks } from "../../lib/api";
3
+ import { Bot, FileCode2, Heart, MessagesSquare, Puzzle, Zap } from "lucide-react";
4
+ import { Agents, Artifacts, Mcps, Routines, Tasks } from "../../lib/api";
5
5
  import { t } from "../../i18n";
6
6
 
7
7
  export function Overview({ pid }: { pid: string }) {
@@ -9,12 +9,14 @@ export function Overview({ pid }: { pid: string }) {
9
9
  const routines = useSWR(`/projects/${pid}/routines`, () => Routines.list(pid));
10
10
  const agents = useSWR(`/projects/${pid}/agents`, () => Agents.list(pid));
11
11
  const mcps = useSWR(`/projects/${pid}/mcps`, () => Mcps.list(pid));
12
+ const artifacts = useSWR(`/projects/${pid}/artifacts`, () => Artifacts.list(pid));
12
13
  return (
13
14
  <div className="grid grid-cols-2 gap-4 md:grid-cols-3">
14
15
  <Card title={t("project.overview.tasks_open")} value={tasks.data?.length ?? "…"} href={`/p/${pid}/tasks`} icon={Zap} />
15
16
  <Card title={t("project.overview.routines")} value={routines.data?.length ?? "…"} href={`/p/${pid}/routines`} icon={Heart} />
16
17
  <Card title={t("project.overview.agents")} value={agents.data?.length ?? "…"} href={`/p/${pid}/agents`} icon={Bot} />
17
18
  <Card title={t("project.overview.mcps")} value={mcps.data?.length ?? "…"} href={`/p/${pid}/mcps`} icon={Puzzle} />
19
+ <Card title={t("project.overview.artifacts")} value={artifacts.data?.length ?? "…"} href={`/p/${pid}/artifacts`} icon={FileCode2} />
18
20
  <Card title={t("project.overview.chat")} value={t("project.overview.chat_value")} href={`/p/${pid}/chat`} icon={MessagesSquare} />
19
21
  </div>
20
22
  );