@agentprojectcontext/apx 1.39.1 → 1.40.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.
Files changed (51) hide show
  1. package/package.json +1 -1
  2. package/src/core/agent/constants.js +7 -1
  3. package/src/core/agent/retry.js +9 -0
  4. package/src/core/agent/run-agent.js +56 -5
  5. package/src/core/agent/tools/pseudo-tools.js +13 -1
  6. package/src/core/channels/telegram/dispatch.js +23 -3
  7. package/src/core/engines/mock.js +33 -10
  8. package/src/core/i18n/en.js +2 -4
  9. package/src/core/i18n/es.js +1 -4
  10. package/src/core/i18n/index.js +5 -1
  11. package/src/core/i18n/pt.js +1 -3
  12. package/src/core/routines/runner.js +15 -3
  13. package/src/host/daemon/api/admin.js +29 -0
  14. package/src/interfaces/web/dist/assets/index-Cg-uHCex.js +646 -0
  15. package/src/interfaces/web/dist/assets/index-Cg-uHCex.js.map +1 -0
  16. package/src/interfaces/web/dist/assets/index-wrEbTJbc.css +1 -0
  17. package/src/interfaces/web/dist/index.html +2 -2
  18. package/src/interfaces/web/src/App.tsx +22 -11
  19. package/src/interfaces/web/src/components/AddProjectDialog.tsx +66 -34
  20. package/src/interfaces/web/src/components/ModelCombobox.tsx +6 -3
  21. package/src/interfaces/web/src/components/chat/MessageBubble.tsx +28 -25
  22. package/src/interfaces/web/src/components/chat/ModelPicker.tsx +19 -17
  23. package/src/interfaces/web/src/components/deck/WidgetRow.tsx +9 -7
  24. package/src/interfaces/web/src/components/inputs/VarTokenInput.tsx +21 -19
  25. package/src/interfaces/web/src/components/layout/ProjectSidebar.tsx +3 -2
  26. package/src/interfaces/web/src/components/routines/AvailableVarsCard.tsx +23 -0
  27. package/src/interfaces/web/src/components/routines/ExecutionsList.tsx +189 -0
  28. package/src/interfaces/web/src/components/routines/ReadOnlyBlock.tsx +14 -0
  29. package/src/interfaces/web/src/components/routines/RoutineDetail.tsx +86 -0
  30. package/src/interfaces/web/src/components/routines/RoutineEditor.tsx +263 -0
  31. package/src/interfaces/web/src/components/routines/RoutineList.tsx +59 -0
  32. package/src/interfaces/web/src/components/routines/VarTextarea.tsx +70 -0
  33. package/src/interfaces/web/src/components/routines/shared.ts +89 -0
  34. package/src/interfaces/web/src/components/settings/PairDeviceDialog.tsx +19 -16
  35. package/src/interfaces/web/src/components/settings/TelegramContactsPanel.tsx +10 -8
  36. package/src/interfaces/web/src/components/settings/providers/ProviderModal.tsx +7 -4
  37. package/src/interfaces/web/src/components/ui/chat-input.tsx +24 -21
  38. package/src/interfaces/web/src/components/ui/sidebar.tsx +20 -18
  39. package/src/interfaces/web/src/components/ui.tsx +4 -0
  40. package/src/interfaces/web/src/i18n/en.ts +34 -11
  41. package/src/interfaces/web/src/i18n/es.ts +34 -11
  42. package/src/interfaces/web/src/lib/api/filesystem.ts +6 -0
  43. package/src/interfaces/web/src/screens/ApxAdminScreen.tsx +11 -3
  44. package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +6 -3
  45. package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +8 -5
  46. package/src/interfaces/web/src/screens/project/McpsTab.tsx +16 -9
  47. package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +126 -373
  48. package/src/interfaces/web/src/styles.css +5 -0
  49. package/src/interfaces/web/dist/assets/index-CAKEYko0.css +0 -1
  50. package/src/interfaces/web/dist/assets/index-UzqHxD0B.js +0 -639
  51. package/src/interfaces/web/dist/assets/index-UzqHxD0B.js.map +0 -1
@@ -1,6 +1,7 @@
1
1
  import { useEffect, useMemo, useState } from "react";
2
2
  import { Braces, Loader2, RefreshCw } from "lucide-react";
3
3
  import { Button, Dialog, Field, Input, Switch, Textarea } from "../../ui";
4
+ import { Tip } from "../../ui/tip";
4
5
  import { UiSelect } from "../../UiSelect";
5
6
  import { ModelCombobox } from "../../ModelCombobox";
6
7
  import { Engines } from "../../../lib/api";
@@ -363,10 +364,12 @@ export function ProviderModal({ open, initial, existingSlugs, onClose, onSave }:
363
364
  options={modelOptions}
364
365
  className="flex-1"
365
366
  />
366
- <Button size="sm" variant="secondary" onClick={loadModels} disabled={loadingModels} title={t("providers_modal.list_models_hint")} aria-label={t("providers_modal.list_models_hint")}>
367
- {loadingModels ? <Loader2 className="size-3.5 animate-spin" /> : <RefreshCw className="size-3.5" />}
368
- {t("providers_modal.load_models")}
369
- </Button>
367
+ <Tip content={t("providers_modal.list_models_hint")}>
368
+ <Button size="sm" variant="secondary" onClick={loadModels} disabled={loadingModels} aria-label={t("providers_modal.list_models_hint")}>
369
+ {loadingModels ? <Loader2 className="size-3.5 animate-spin" /> : <RefreshCw className="size-3.5" />}
370
+ {t("providers_modal.load_models")}
371
+ </Button>
372
+ </Tip>
370
373
  </div>
371
374
  {modelError && <p className="text-[11px] text-amber-400">{modelError}</p>}
372
375
  </div>
@@ -5,6 +5,7 @@ import { ArrowUp, Square } from "lucide-react"
5
5
 
6
6
  import { cn } from "@/lib/utils"
7
7
  import { Button } from "@/components/ui/button"
8
+ import { Tip } from "./tip"
8
9
  import { t } from "@/i18n"
9
10
 
10
11
  interface ChatInputProps {
@@ -105,28 +106,30 @@ export function ChatInput({
105
106
  {footer}
106
107
  </div>
107
108
  {busy && onStop ? (
108
- <Button
109
- type="button"
110
- size="icon-sm"
111
- variant="destructive"
112
- onClick={onStop}
113
- aria-label={t("chat_ui.stop")}
114
- title={t("chat_ui.stop")}
115
- >
116
- <Square className="size-3.5" fill="currentColor" />
117
- </Button>
109
+ <Tip content={t("chat_ui.stop")}>
110
+ <Button
111
+ type="button"
112
+ size="icon-sm"
113
+ variant="destructive"
114
+ onClick={onStop}
115
+ aria-label={t("chat_ui.stop")}
116
+ >
117
+ <Square className="size-3.5" fill="currentColor" />
118
+ </Button>
119
+ </Tip>
118
120
  ) : (
119
- <Button
120
- type="button"
121
- size="icon-sm"
122
- variant="default"
123
- onClick={onSubmit}
124
- disabled={!canSend}
125
- aria-label={t("chat_ui.send")}
126
- title={t("chat_ui.send")}
127
- >
128
- <ArrowUp className="size-4" />
129
- </Button>
121
+ <Tip content={t("chat_ui.send")}>
122
+ <Button
123
+ type="button"
124
+ size="icon-sm"
125
+ variant="default"
126
+ onClick={onSubmit}
127
+ disabled={!canSend}
128
+ aria-label={t("chat_ui.send")}
129
+ >
130
+ <ArrowUp className="size-4" />
131
+ </Button>
132
+ </Tip>
130
133
  )}
131
134
  </div>
132
135
  </div>
@@ -19,6 +19,7 @@ import {
19
19
  SheetTitle,
20
20
  } from "@/components/ui/sheet"
21
21
  import { Skeleton } from "@/components/ui/skeleton"
22
+ import { Tip } from "./tip"
22
23
  import {
23
24
  Tooltip,
24
25
  TooltipContent,
@@ -282,24 +283,25 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) {
282
283
  const { toggleSidebar } = useSidebar()
283
284
 
284
285
  return (
285
- <button
286
- data-sidebar="rail"
287
- data-slot="sidebar-rail"
288
- aria-label={t("sidebar_ui.toggle")}
289
- tabIndex={-1}
290
- onClick={toggleSidebar}
291
- title={t("sidebar_ui.toggle")}
292
- className={cn(
293
- "absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] hover:after:bg-sidebar-border sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2",
294
- "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
295
- "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
296
- "group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-sidebar",
297
- "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
298
- "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
299
- className
300
- )}
301
- {...props}
302
- />
286
+ <Tip content={t("sidebar_ui.toggle")}>
287
+ <button
288
+ data-sidebar="rail"
289
+ data-slot="sidebar-rail"
290
+ aria-label={t("sidebar_ui.toggle")}
291
+ tabIndex={-1}
292
+ onClick={toggleSidebar}
293
+ className={cn(
294
+ "absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] hover:after:bg-sidebar-border sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2",
295
+ "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize",
296
+ "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize",
297
+ "group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:left-full hover:group-data-[collapsible=offcanvas]:bg-sidebar",
298
+ "[[data-side=left][data-collapsible=offcanvas]_&]:-right-2",
299
+ "[[data-side=right][data-collapsible=offcanvas]_&]:-left-2",
300
+ className
301
+ )}
302
+ {...props}
303
+ />
304
+ </Tip>
303
305
  )
304
306
  }
305
307
 
@@ -12,6 +12,10 @@ import { Switch as SSwitch } from "./ui/switch";
12
12
  import { Spinner as SSpinner } from "./ui/spinner";
13
13
  import { Dialog as DialogRoot, DialogContent, DialogHeader, DialogTitle, DialogDescription } from "./ui/dialog";
14
14
 
15
+ // Re-export the tooltip convenience wrapper so call sites can grab it from the
16
+ // same barrel as Button/Field/etc.: `import { Button, Tip } from "../ui"`.
17
+ export { Tip } from "./ui/tip";
18
+
15
19
  // ── Button ──────────────────────────────────────────────────────────────────
16
20
  type Variant = "primary" | "secondary" | "ghost" | "destructive";
17
21
  type Size = "sm" | "md";
@@ -128,6 +128,7 @@ export const en = {
128
128
  path_required: "Path required.",
129
129
  registered: "Project #{id} registered.",
130
130
  search_btn: "Browse",
131
+ picker_prompt: "Pick the project folder",
131
132
  browser_unavailable: "Browser unavailable until daemon restarts. Paste path manually.",
132
133
  no_folders: "No folders.",
133
134
  },
@@ -368,6 +369,7 @@ export const en = {
368
369
  new: "new",
369
370
  new_btn: "New",
370
371
  delete_confirm: "Delete routine {name}?",
372
+ delete_confirm_body: "This can't be undone.",
371
373
  saved: "Routine saved.",
372
374
  paused: "paused",
373
375
  next_run: "next:",
@@ -385,6 +387,24 @@ export const en = {
385
387
  schedule_hint: "Choose a preset or type manually. Manual = only runs via Run button.",
386
388
  vars_title: "Available variables",
387
389
  what_happens: "What will happen",
390
+ list_title: "Routines",
391
+ detail_empty: "Pick a routine from the list.",
392
+ edit_btn: "Edit",
393
+ edit_hint: "Open the editor: kind, schedule, prompt, pre/post and variables.",
394
+ block_pre: "Pre-commands",
395
+ block_post: "Post-commands",
396
+ block_prompt: "Prompt",
397
+ block_text: "Text",
398
+ block_command: "Command",
399
+ block_empty: "(empty)",
400
+ runs_title: "Executions",
401
+ runs_empty: "No executions yet.",
402
+ runs_close: "Close",
403
+ runs_no_detail: "No further detail.",
404
+ runs_output: "Output",
405
+ status_ok: "ok",
406
+ status_error: "error",
407
+ status_skipped: "skipped",
388
408
  agent_field: "Agent (spec.agent)",
389
409
  agent_hint: "Who executes the routine.",
390
410
  agent_loading: "loading…",
@@ -399,7 +419,7 @@ export const en = {
399
419
  post_hint: "Shell AFTER the prompt. One per line.",
400
420
  tg_channel: "Channel (spec.channel)",
401
421
  tg_chat_id: "Chat ID (spec.chat_id)",
402
- tg_text: "Text (spec.text)",
422
+ tg_text: "Telegram Message (spec.text)",
403
423
  tg_text_hint: "Fixed message to send. Does not use a model.",
404
424
  shell_field: "Command (spec.command)",
405
425
  shell_hint: "Runs as-is in the shell. No prompt, no pre/post.",
@@ -411,13 +431,16 @@ export const en = {
411
431
  toggle_error: "toggle failed",
412
432
  delete_error: "delete failed",
413
433
  run_success: "{name} fired.",
434
+ run_confirm: "Run routine {name} now?",
435
+ run_confirm_body: "Runs the action once, regardless of the schedule.",
436
+ running: "Running…",
414
437
  delete_success: "deleted.",
415
438
  },
416
439
 
417
440
  agents: {
418
441
  title: "Agents",
419
- subtitle: "Defined in AGENTS.md + .apc/agents/<slug>.md.",
420
- subtitle_full: "Defined in AGENTS.md + .apc/agents/<slug>.md. Runtime memory lives under ~/.apx/projects/<id>/agents/<slug>/.",
442
+ subtitle: "Defined in .apc/agents/<slug>.md.",
443
+ subtitle_full: "Defined in .apc/agents/<slug>.md. Runtime memory lives under ~/.apx/projects/<id>/agents/<slug>/.",
421
444
  empty: "No agents. Add one with <code>apx agent add</code> or the button.",
422
445
  empty_text: "No agents. Add one with `apx agent add` or the button above.",
423
446
  new: "Agent",
@@ -1243,14 +1266,14 @@ export const en = {
1243
1266
  preset_daily_9am: "daily 9am",
1244
1267
  preset_weekdays_9am: "weekdays 9am",
1245
1268
  preset_manual: "Manual",
1246
- var_pre_output_prompt: "Output of the pre-commands, injected into the prompt.",
1247
- var_llm_output: "Response from the agent / super-agent.",
1248
- var_status: "ok | error.",
1249
- var_skipped: "1 if the action was skipped.",
1250
- var_pre_output: "Output of the pre-commands.",
1251
- var_pre_output_file: "File with the pre output (for large outputs).",
1252
- var_pre_exit: "Exit code of the pre-commands.",
1253
- var_routine: "Name of the routine.",
1269
+ var_pre_output_prompt: "Text output of the pre-commands. Replaced inside the prompt/text before it is sent. Use it to inject fresh data (weather, an API) into the instruction.",
1270
+ var_llm_output: "Final answer from the agent or super-agent. Available in the post-commands as an env var — e.g. forward it via Telegram.",
1271
+ var_status: "Action result: ok or error. Available in the post-commands to branch on what happened.",
1272
+ var_skipped: "1 if the action was skipped (via skip_prompt_on), 0 if it ran. Available in the post-commands.",
1273
+ var_pre_output: "Full pre-commands output, as an env var in the post-commands (up to 32k).",
1274
+ var_pre_output_file: "Path to a temp file with the pre-commands output. For large outputs not suited to an env var.",
1275
+ var_pre_exit: "Exit code of the last pre-command (0 = ok). Available in the post-commands.",
1276
+ var_routine: "Name of this routine. Available as an env var in the commands.",
1254
1277
  summary_runs_agent: "Runs the agent \"{agent}\"",
1255
1278
  summary_runs_agent_none: "Runs an agent (none chosen yet)",
1256
1279
  summary_super_agent: "Calls the super-agent",
@@ -129,6 +129,7 @@ export const es = {
129
129
  path_required: "Ruta requerida.",
130
130
  registered: "Proyecto #{id} registrado.",
131
131
  search_btn: "Buscar",
132
+ picker_prompt: "Elegí la carpeta del proyecto",
132
133
  browser_unavailable: "Explorador no disponible hasta reiniciar daemon. Pegá ruta manual.",
133
134
  no_folders: "Sin carpetas.",
134
135
  },
@@ -369,6 +370,7 @@ export const es = {
369
370
  new: "nueva",
370
371
  new_btn: "Nueva",
371
372
  delete_confirm: "Borrar rutina {name}?",
373
+ delete_confirm_body: "Esta acción no se puede deshacer.",
372
374
  saved: "Rutina guardada.",
373
375
  paused: "pausada",
374
376
  next_run: "próxima:",
@@ -386,6 +388,24 @@ export const es = {
386
388
  schedule_hint: "Elegí un preset o escribilo a mano. Manual = solo corre con el botón Run.",
387
389
  vars_title: "Variables disponibles",
388
390
  what_happens: "Qué va a pasar",
391
+ list_title: "Rutinas",
392
+ detail_empty: "Elegí una rutina de la lista.",
393
+ edit_btn: "Editar",
394
+ edit_hint: "Abrir el editor: tipo, intervalo, prompt, pre/post y variables.",
395
+ block_pre: "Pre-commands",
396
+ block_post: "Post-commands",
397
+ block_prompt: "Prompt",
398
+ block_text: "Texto",
399
+ block_command: "Comando",
400
+ block_empty: "(vacío)",
401
+ runs_title: "Ejecuciones",
402
+ runs_empty: "Sin ejecuciones todavía.",
403
+ runs_close: "Cerrar",
404
+ runs_no_detail: "Sin más detalle.",
405
+ runs_output: "Salida",
406
+ status_ok: "ok",
407
+ status_error: "error",
408
+ status_skipped: "salteada",
389
409
  agent_field: "Agente (spec.agent)",
390
410
  agent_hint: "Quién ejecuta la rutina.",
391
411
  agent_loading: "cargando…",
@@ -400,7 +420,7 @@ export const es = {
400
420
  post_hint: "Shell DESPUÉS del prompt. Uno por línea.",
401
421
  tg_channel: "Canal (spec.channel)",
402
422
  tg_chat_id: "Chat ID (spec.chat_id)",
403
- tg_text: "Texto (spec.text)",
423
+ tg_text: "Mensaje de Telegram (spec.text)",
404
424
  tg_text_hint: "Mensaje fijo a enviar. No usa modelo.",
405
425
  shell_field: "Comando (spec.command)",
406
426
  shell_hint: "Corre tal cual en el shell. Sin prompt ni pre/post.",
@@ -412,13 +432,16 @@ export const es = {
412
432
  toggle_error: "toggle falló",
413
433
  delete_error: "delete falló",
414
434
  run_success: "{name} disparada.",
435
+ run_confirm: "¿Ejecutar la rutina {name} ahora?",
436
+ run_confirm_body: "Corre la acción una vez, sin esperar al horario.",
437
+ running: "Ejecutando…",
415
438
  delete_success: "borrada.",
416
439
  },
417
440
 
418
441
  agents: {
419
442
  title: "Agents",
420
- subtitle: "Definidos en AGENTS.md + .apc/agents/<slug>.md.",
421
- subtitle_full: "Definidos en AGENTS.md + .apc/agents/<slug>.md. La memoria runtime vive en ~/.apx/projects/<id>/agents/<slug>/.",
443
+ subtitle: "Definidos en .apc/agents/<slug>.md.",
444
+ subtitle_full: "Definidos en .apc/agents/<slug>.md. La memoria runtime vive en ~/.apx/projects/<id>/agents/<slug>/.",
422
445
  empty: "Sin agents. Agregá uno con <code>apx agent add</code> o el botón.",
423
446
  empty_text: "Sin agents. Agregá uno con `apx agent add` o el botón de arriba.",
424
447
  new: "Agente",
@@ -1241,14 +1264,14 @@ export const es = {
1241
1264
  preset_daily_9am: "diario 9am",
1242
1265
  preset_weekdays_9am: "días hábiles 9am",
1243
1266
  preset_manual: "Manual",
1244
- var_pre_output_prompt: "Salida de los pre-commands, inyectada en el prompt.",
1245
- var_llm_output: "Respuesta del agente / super-agente.",
1246
- var_status: "ok | error.",
1247
- var_skipped: "1 si la acción se salteó.",
1248
- var_pre_output: "Salida de los pre-commands.",
1249
- var_pre_output_file: "Archivo con la salida pre (para salidas grandes).",
1250
- var_pre_exit: "Código de salida de los pre-commands.",
1251
- var_routine: "Nombre de la rutina.",
1267
+ var_pre_output_prompt: "Salida de texto de los pre-commands. Se reemplaza dentro del prompt/texto antes de enviarlo. Útil para inyectar datos frescos (clima, una API) en la instrucción.",
1268
+ var_llm_output: "Respuesta final del agente o super-agente. Disponible en los post-commands como variable de entorno. Ej: reenviarla por Telegram.",
1269
+ var_status: "Resultado de la acción: ok o error. Disponible en los post-commands para decidir qué hacer después.",
1270
+ var_skipped: "Vale 1 si la acción se salteó (por skip_prompt_on), 0 si corrió. Disponible en los post-commands.",
1271
+ var_pre_output: "Salida completa de los pre-commands, como variable de entorno en los post-commands (hasta 32k).",
1272
+ var_pre_output_file: "Ruta a un archivo temporal con la salida de los pre-commands. Para salidas grandes que no convienen como variable.",
1273
+ var_pre_exit: "Código de salida del último pre-command (0 = ok). Disponible en los post-commands.",
1274
+ var_routine: "Nombre de esta rutina. Disponible como variable de entorno en los comandos.",
1252
1275
  summary_runs_agent: "Corre el agente \"{agent}\"",
1253
1276
  summary_runs_agent_none: "Corre un agente (todavía no elegiste cuál)",
1254
1277
  summary_super_agent: "Llama al super-agente",
@@ -6,7 +6,13 @@ export interface DirectoryList {
6
6
  entries: string[];
7
7
  }
8
8
 
9
+ export type PickDirResult = { path: string } | { cancelled: true };
10
+
9
11
  export const Filesystem = {
10
12
  dirs: (path: string) =>
11
13
  http.get<DirectoryList>(`/admin/fs/dirs?path=${encodeURIComponent(path)}`),
14
+ pickDir: (prompt?: string) =>
15
+ http.get<PickDirResult>(
16
+ `/admin/fs/pick-dir${prompt ? `?prompt=${encodeURIComponent(prompt)}` : ""}`,
17
+ ),
12
18
  };
@@ -1,8 +1,9 @@
1
1
  import { useState } from "react";
2
- import { useNavigate } from "react-router-dom";
2
+ import { useNavigate, useSearchParams } from "react-router-dom";
3
3
  import { Plus, Send } from "lucide-react";
4
4
  import { Section, StatusDot } from "../components/Section";
5
5
  import { Badge, Button, Empty, Loading, Switch } from "../components/ui";
6
+ import { Tip } from "../components/ui/tip";
6
7
  import { Admin, Projects, Telegram } from "../lib/api";
7
8
  import { useToast } from "../components/Toast";
8
9
  import { useDaemonStatus } from "../hooks/useDaemonStatus";
@@ -17,6 +18,7 @@ import type { TelegramChannel } from "../types/daemon";
17
18
 
18
19
  export function ApxAdminScreen() {
19
20
  const navigate = useNavigate();
21
+ const [params, setParams] = useSearchParams();
20
22
  const toast = useToast();
21
23
  const { health, isUp } = useDaemonStatus();
22
24
  const { projects, isLoading: projLoading, mutate: mutateProjects } = useProjects();
@@ -57,8 +59,14 @@ export function ApxAdminScreen() {
57
59
  <p className="text-sm text-muted-fg">{t("admin.subtitle")}</p>
58
60
  </div>
59
61
  <div className="flex gap-2">
60
- <Button size="sm" onClick={reload} title={t("daemon.reload_hint")}>{t("common.reload")} config</Button>
61
- <Button size="sm" variant="primary" onClick={() => navigate("/?action=add-project")}>
62
+ <Tip content={t("daemon.reload_hint")}>
63
+ <Button size="sm" onClick={reload}>{t("common.reload")} config</Button>
64
+ </Tip>
65
+ <Button size="sm" variant="primary" onClick={() => {
66
+ const next = new URLSearchParams(params);
67
+ next.set("action", "add-project");
68
+ setParams(next);
69
+ }}>
62
70
  <Plus size={14} /> {t("nav.project")}
63
71
  </Button>
64
72
  </div>
@@ -3,6 +3,7 @@ import { RefreshCw } from "lucide-react";
3
3
  import { Deck } from "../../lib/api/deck";
4
4
  import { Section } from "../../components/Section";
5
5
  import { Button, Empty, Loading } from "../../components/ui";
6
+ import { Tip } from "../../components/ui/tip";
6
7
  import { useToast } from "../../components/Toast";
7
8
  import { DaemonCard } from "../../components/deck/DaemonCard";
8
9
  import { DesktopGroup } from "../../components/deck/DesktopGroup";
@@ -91,9 +92,11 @@ export function DeckScreen() {
91
92
  : t("modules_ui.deck_widgets_summary", { count: widgets.length, enabled: enabledCount })
92
93
  }
93
94
  action={
94
- <Button size="sm" variant="ghost" onClick={() => mutate()} disabled={isLoading} title={t("deck_screen.reload_manifest")} aria-label={t("deck_screen.reload_manifest")}>
95
- <RefreshCw size={14} className={isLoading ? "animate-spin" : ""} />
96
- </Button>
95
+ <Tip content={t("deck_screen.reload_manifest")}>
96
+ <Button size="sm" variant="ghost" onClick={() => mutate()} disabled={isLoading} aria-label={t("deck_screen.reload_manifest")}>
97
+ <RefreshCw size={14} className={isLoading ? "animate-spin" : ""} />
98
+ </Button>
99
+ </Tip>
97
100
  }
98
101
  >
99
102
  {isLoading && <Loading label={t("modules_ui.deck_loading_manifest_full")} />}
@@ -9,6 +9,7 @@ import { Agents, Conversations, Messages, Routines, Tasks, Tools } from "../../l
9
9
  import type { AgentDetail, AgentEntry, MessageEntry, RoutineEntry } from "../../types/daemon";
10
10
  import { Section } from "../../components/Section";
11
11
  import { Badge, Button, Field, Input, Loading, Switch, Textarea } from "../../components/ui";
12
+ import { Tip } from "../../components/ui/tip";
12
13
  import { UiSelect } from "../../components/UiSelect";
13
14
  import { useToast } from "../../components/Toast";
14
15
  import { cn } from "../../lib/cn";
@@ -405,11 +406,13 @@ function ToolsPicker({ value, onChange }: { value: string; onChange: (v: string)
405
406
  {catalog.map((tool) => {
406
407
  const on = selected.includes(tool.name);
407
408
  return (
408
- <button key={tool.name} type="button" title={tool.description || tool.name} onClick={() => toggle(tool.name)}
409
- className={cn("rounded-md border px-2 py-0.5 font-mono text-[11px] transition-colors",
410
- on ? "border-emerald-500/50 bg-emerald-500/10 text-emerald-400" : "border-border text-muted-fg hover:text-foreground")}>
411
- {tool.name}
412
- </button>
409
+ <Tip key={tool.name} content={tool.description || tool.name}>
410
+ <button type="button" onClick={() => toggle(tool.name)}
411
+ className={cn("rounded-md border px-2 py-0.5 font-mono text-[11px] transition-colors",
412
+ on ? "border-emerald-500/50 bg-emerald-500/10 text-emerald-400" : "border-border text-muted-fg hover:text-foreground")}>
413
+ {tool.name}
414
+ </button>
415
+ </Tip>
413
416
  );
414
417
  })}
415
418
  {custom.map((s) => (
@@ -7,6 +7,7 @@ import { Mcps, Vars, type McpAddBody, type McpScope, type McpTestResult, type Mc
7
7
  import type { McpEntry } from "../../types/daemon";
8
8
  import { Section } from "../../components/Section";
9
9
  import { Badge, Button, Dialog, Empty, Field, Input, Loading, Switch } from "../../components/ui";
10
+ import { Tip } from "../../components/ui/tip";
10
11
  import { UiSelect } from "../../components/UiSelect";
11
12
  import { VarTokenInput } from "../../components/inputs/VarTokenInput";
12
13
  import { KeyValueList, recordFromRows, rowsFromRecord, type KvRow } from "../../components/inputs/KeyValueList";
@@ -122,16 +123,22 @@ export function McpsTab({ pid }: { pid: string }) {
122
123
  label=""
123
124
  />
124
125
  </div>
125
- <Button size="sm" variant="ghost" onClick={(e) => { e.stopPropagation(); runTest(m.name); }} aria-label={t("project.mcps.test_btn")} title={t("project.mcps.test_btn")}>
126
- <FlaskConical size={13} />
127
- </Button>
128
- <Button size="sm" variant="ghost" onClick={(e) => { e.stopPropagation(); setActiveMcp(m.name); }} aria-label={t("project.mcps.logs_btn")} title={t("project.mcps.logs_btn")}>
129
- <ScrollText size={13} />
130
- </Button>
131
- {writable && (
132
- <Button size="sm" variant="ghost" onClick={(e) => { e.stopPropagation(); setDialog({ kind: "edit", entry: m }); }} aria-label={t("project.mcps.edit_btn")} title={t("project.mcps.edit_btn")}>
133
- <Pencil size={13} />
126
+ <Tip content={t("project.mcps.test_btn")}>
127
+ <Button size="sm" variant="ghost" onClick={(e) => { e.stopPropagation(); runTest(m.name); }} aria-label={t("project.mcps.test_btn")}>
128
+ <FlaskConical size={13} />
129
+ </Button>
130
+ </Tip>
131
+ <Tip content={t("project.mcps.logs_btn")}>
132
+ <Button size="sm" variant="ghost" onClick={(e) => { e.stopPropagation(); setActiveMcp(m.name); }} aria-label={t("project.mcps.logs_btn")}>
133
+ <ScrollText size={13} />
134
134
  </Button>
135
+ </Tip>
136
+ {writable && (
137
+ <Tip content={t("project.mcps.edit_btn")}>
138
+ <Button size="sm" variant="ghost" onClick={(e) => { e.stopPropagation(); setDialog({ kind: "edit", entry: m }); }} aria-label={t("project.mcps.edit_btn")}>
139
+ <Pencil size={13} />
140
+ </Button>
141
+ </Tip>
135
142
  )}
136
143
  {writable && (
137
144
  <Button size="sm" variant="destructive" onClick={(e) => { e.stopPropagation(); remove(m.name, scopeForRemove); }}>