@agentprojectcontext/apx 1.33.0 → 1.34.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 (172) hide show
  1. package/package.json +1 -1
  2. package/skills/apc-context/SKILL.md +2 -5
  3. package/skills/apx/SKILL.md +49 -61
  4. package/src/core/agent/a2a/reply.js +48 -0
  5. package/src/core/agent/build-agent-system.js +4 -3
  6. package/src/core/agent/channels/voice-context.js +98 -0
  7. package/src/core/agent/memory.js +2 -1
  8. package/src/core/agent/prompt-builder.js +2 -1
  9. package/src/core/agent/prompts/modes/code-build.md +1 -0
  10. package/src/core/agent/prompts/modes/code-plan.md +1 -0
  11. package/src/core/agent/prompts/modes/index.js +28 -0
  12. package/src/core/agent/skills/loader.js +22 -18
  13. package/src/core/agent/stream/turn-accumulator.js +73 -0
  14. package/src/core/agent/suggestions.js +37 -0
  15. package/src/core/agent/tools/handlers/add-project.js +5 -2
  16. package/src/core/agent/tools/handlers/call-runtime.js +3 -2
  17. package/src/core/agent/tools/handlers/transcribe-audio.js +1 -1
  18. package/src/core/agent/tools/helpers.js +2 -2
  19. package/src/core/agent/tools/names.js +138 -0
  20. package/src/core/agent/tools/registry-bridge.js +6 -14
  21. package/src/core/agent/tools/registry.js +68 -65
  22. package/src/core/apc/context-copy.js +27 -0
  23. package/src/core/apc/notes.js +19 -0
  24. package/src/core/apc/parser.js +13 -6
  25. package/src/core/apc/paths.js +87 -0
  26. package/src/core/apc/scaffold.js +82 -74
  27. package/src/core/apc/skill-sync.js +13 -1
  28. package/src/core/channels/telegram/dispatch.js +595 -0
  29. package/src/core/channels/telegram/helpers.js +130 -0
  30. package/src/core/config/index.js +3 -2
  31. package/src/core/config/redact.js +95 -0
  32. package/src/core/constants/channels.js +2 -0
  33. package/src/core/constants/code-modes.js +10 -0
  34. package/src/core/constants/index.js +1 -0
  35. package/src/core/deck/manifest.js +186 -0
  36. package/src/core/engines/catalog.js +83 -0
  37. package/src/core/engines/gemini.js +28 -11
  38. package/src/core/engines/index.js +11 -1
  39. package/src/core/{tools → http-tools}/browser.js +0 -1
  40. package/src/core/{tools → http-tools}/fetch.js +0 -1
  41. package/src/core/{tools → http-tools}/glob.js +0 -1
  42. package/src/core/{tools → http-tools}/grep.js +0 -1
  43. package/src/core/{tools → http-tools}/registry.js +0 -1
  44. package/src/core/{tools → http-tools}/search.js +0 -1
  45. package/src/core/i18n/en.js +9 -0
  46. package/src/core/i18n/es.js +12 -0
  47. package/src/core/i18n/index.js +54 -0
  48. package/src/core/i18n/pt.js +9 -0
  49. package/src/core/identity/telegram.js +2 -1
  50. package/src/core/mcp/runner.js +272 -14
  51. package/src/core/mcp/sources.js +3 -2
  52. package/src/core/routines/index.js +16 -0
  53. package/src/{host/daemon/routines.js → core/routines/runner.js} +36 -103
  54. package/src/core/runtime-skills/apc-context/SKILL.md +159 -0
  55. package/src/core/runtime-skills/apx/SKILL.md +95 -0
  56. package/src/core/runtime-skills/apx-mcp/SKILL.md +116 -0
  57. package/src/core/runtime-skills/{claude-code.md → claude-code/SKILL.md} +1 -0
  58. package/src/core/runtime-skills/{codex-cli.md → codex-cli/SKILL.md} +1 -0
  59. package/src/core/runtime-skills/{opencode-cli.md → opencode-cli/SKILL.md} +1 -0
  60. package/src/core/runtime-skills/{openrouter.md → openrouter/SKILL.md} +1 -0
  61. package/src/{host/daemon/env-detect.js → core/runtimes/detect.js} +1 -1
  62. package/src/core/stores/code-sessions.js +50 -2
  63. package/src/core/stores/routine-memory.js +1 -1
  64. package/src/core/stores/sessions-search.js +121 -0
  65. package/src/core/stores/sessions.js +38 -0
  66. package/src/core/vars/index.js +14 -0
  67. package/src/core/vars/interpolate.js +86 -0
  68. package/src/core/vars/sources.js +151 -0
  69. package/src/core/voice/audio-decode.js +38 -0
  70. package/src/core/voice/transcription.js +225 -0
  71. package/src/host/daemon/api/admin-config.js +5 -82
  72. package/src/host/daemon/api/agents.js +5 -5
  73. package/src/host/daemon/api/code.js +17 -169
  74. package/src/host/daemon/api/config.js +3 -4
  75. package/src/host/daemon/api/conversations.js +8 -29
  76. package/src/host/daemon/api/deck.js +37 -404
  77. package/src/host/daemon/api/engines.js +1 -50
  78. package/src/host/daemon/api/exec.js +1 -1
  79. package/src/host/daemon/api/mcps.js +32 -0
  80. package/src/host/daemon/api/routines.js +1 -1
  81. package/src/host/daemon/api/runtimes.js +4 -3
  82. package/src/host/daemon/api/sessions-search.js +24 -140
  83. package/src/host/daemon/api/sessions.js +12 -30
  84. package/src/host/daemon/api/shared.js +2 -1
  85. package/src/host/daemon/api/telegram.js +1 -11
  86. package/src/host/daemon/api/tools.js +6 -6
  87. package/src/host/daemon/api/transcribe.js +2 -2
  88. package/src/host/daemon/api/vars.js +137 -0
  89. package/src/host/daemon/api/voice.js +13 -290
  90. package/src/host/daemon/api.js +2 -0
  91. package/src/host/daemon/db.js +6 -6
  92. package/src/host/daemon/deck-exec.js +148 -0
  93. package/src/host/daemon/index.js +3 -3
  94. package/src/host/daemon/plugins/telegram/index.js +24 -687
  95. package/src/host/daemon/routines-scheduler.js +64 -0
  96. package/src/host/daemon/smoke.js +3 -2
  97. package/src/host/daemon/whisper-server.js +225 -0
  98. package/src/interfaces/cli/commands/agent.js +3 -2
  99. package/src/interfaces/cli/commands/command.js +2 -3
  100. package/src/interfaces/cli/commands/messages.js +6 -2
  101. package/src/interfaces/cli/commands/pair.js +5 -4
  102. package/src/interfaces/cli/commands/search.js +1 -1
  103. package/src/interfaces/cli/commands/sessions.js +3 -2
  104. package/src/interfaces/cli/commands/skills.js +36 -55
  105. package/src/interfaces/web/dist/assets/index-DdmSRtsz.css +1 -0
  106. package/src/interfaces/web/dist/assets/index-M4FspaCH.js +613 -0
  107. package/src/interfaces/web/dist/assets/index-M4FspaCH.js.map +1 -0
  108. package/src/interfaces/web/dist/index.html +2 -2
  109. package/src/interfaces/web/package-lock.json +182 -182
  110. package/src/interfaces/web/src/components/ModelCombobox.tsx +44 -8
  111. package/src/interfaces/web/src/components/TelegramChannelDialog.tsx +1 -1
  112. package/src/interfaces/web/src/components/chat/AskAnswersCard.tsx +76 -0
  113. package/src/interfaces/web/src/components/chat/MessageBubble.tsx +16 -3
  114. package/src/interfaces/web/src/components/chat/MessageList.tsx +23 -1
  115. package/src/interfaces/web/src/components/chat/ModelPicker.tsx +3 -1
  116. package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +4 -4
  117. package/src/interfaces/web/src/components/code/CodeChangesTab.tsx +1 -1
  118. package/src/interfaces/web/src/components/code/CodeFileTree.tsx +3 -2
  119. package/src/interfaces/web/src/components/code/CodeFileViewer.tsx +3 -2
  120. package/src/interfaces/web/src/components/code/CodeTerminal.tsx +3 -2
  121. package/src/interfaces/web/src/components/config/GlobalConfigEditor.tsx +2 -1
  122. package/src/interfaces/web/src/components/deck/WidgetRow.tsx +2 -1
  123. package/src/interfaces/web/src/components/inputs/KeyValueList.tsx +93 -0
  124. package/src/interfaces/web/src/components/inputs/VarTokenInput.tsx +449 -0
  125. package/src/interfaces/web/src/components/settings/DefaultRouterCard.tsx +2 -1
  126. package/src/interfaces/web/src/components/settings/EnginesPanel.tsx +2 -2
  127. package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +5 -4
  128. package/src/interfaces/web/src/components/settings/providers/ProviderCard.tsx +3 -2
  129. package/src/interfaces/web/src/components/settings/providers/ProviderModal.tsx +3 -2
  130. package/src/interfaces/web/src/components/ui/chat-input.tsx +5 -4
  131. package/src/interfaces/web/src/components/ui/sidebar.tsx +3 -2
  132. package/src/interfaces/web/src/components/voice/VoiceProviderModal.tsx +2 -1
  133. package/src/interfaces/web/src/constants/index.ts +1 -1
  134. package/src/interfaces/web/src/i18n/en.ts +174 -7
  135. package/src/interfaces/web/src/i18n/es.ts +179 -15
  136. package/src/interfaces/web/src/lib/api/mcps.ts +25 -0
  137. package/src/interfaces/web/src/lib/api/vars.ts +38 -0
  138. package/src/interfaces/web/src/lib/api.ts +1 -0
  139. package/src/interfaces/web/src/screens/ProjectScreen.tsx +8 -31
  140. package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +1 -1
  141. package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +4 -3
  142. package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +7 -6
  143. package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +4 -3
  144. package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +1 -1
  145. package/src/interfaces/web/src/screens/project/ConfigTab.tsx +132 -1
  146. package/src/interfaces/web/src/screens/project/McpsTab.tsx +549 -104
  147. package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +1 -1
  148. package/src/interfaces/web/src/screens/project/VarsTab.tsx +300 -0
  149. package/src/interfaces/web/src/types/daemon.ts +5 -0
  150. package/src/host/daemon/transcription.js +0 -538
  151. package/src/host/daemon/whisper-transcribe.py +0 -73
  152. package/src/interfaces/web/dist/assets/index-7dVT2O1S.css +0 -1
  153. package/src/interfaces/web/dist/assets/index-DWsE_8Nz.js +0 -602
  154. package/src/interfaces/web/dist/assets/index-DWsE_8Nz.js.map +0 -1
  155. /package/src/{host/daemon → core/apc}/projects-helpers.js +0 -0
  156. /package/src/{host/daemon/plugins → core/channels}/telegram/ask.js +0 -0
  157. /package/src/{host/daemon/plugins → core/channels}/telegram/media.js +0 -0
  158. /package/src/core/{tools → http-tools}/index.js +0 -0
  159. /package/{skills → src/core/runtime-skills}/apx-agency-agents/SKILL.md +0 -0
  160. /package/{skills → src/core/runtime-skills}/apx-agent/SKILL.md +0 -0
  161. /package/{skills → src/core/runtime-skills}/apx-mcp-builder/SKILL.md +0 -0
  162. /package/{skills → src/core/runtime-skills}/apx-project/SKILL.md +0 -0
  163. /package/{skills → src/core/runtime-skills}/apx-routine/SKILL.md +0 -0
  164. /package/{skills → src/core/runtime-skills}/apx-runtime/SKILL.md +0 -0
  165. /package/{skills → src/core/runtime-skills}/apx-sessions/SKILL.md +0 -0
  166. /package/{skills → src/core/runtime-skills}/apx-skill-builder/SKILL.md +0 -0
  167. /package/{skills → src/core/runtime-skills}/apx-task/SKILL.md +0 -0
  168. /package/{skills → src/core/runtime-skills}/apx-telegram/SKILL.md +0 -0
  169. /package/{skills → src/core/runtime-skills}/apx-voice/SKILL.md +0 -0
  170. /package/src/{host/daemon/compact.js → core/stores/conversations-compactor.js} +0 -0
  171. /package/src/{host/daemon → core/stores}/conversations.js +0 -0
  172. /package/src/{host/daemon → core/util}/thinking.js +0 -0
@@ -7,6 +7,7 @@ import { useToast } from "../../components/Toast";
7
7
  import { DaemonCard } from "../../components/deck/DaemonCard";
8
8
  import { DesktopGroup } from "../../components/deck/DesktopGroup";
9
9
  import type { DeckWidget } from "../../lib/api/deck";
10
+ import { t } from "../../i18n";
10
11
 
11
12
  // Deck module — configure the companion "Deck" app: enable/disable widgets,
12
13
  // view desktops, inspect the daemon manifest.
@@ -81,7 +82,7 @@ export function DeckScreen() {
81
82
 
82
83
  {/* Widgets section */}
83
84
  <Section
84
- title="Widgets"
85
+ title={t("deck_screen.widgets_title")}
85
86
  description={
86
87
  isLoading
87
88
  ? "Cargando manifest…"
@@ -90,7 +91,7 @@ export function DeckScreen() {
90
91
  : `${widgets.length} widgets · ${enabledCount} externos habilitados`
91
92
  }
92
93
  action={
93
- <Button size="sm" variant="ghost" onClick={() => mutate()} disabled={isLoading} title="Recargar manifest">
94
+ <Button size="sm" variant="ghost" onClick={() => mutate()} disabled={isLoading} title={t("deck_screen.reload_manifest")} aria-label={t("deck_screen.reload_manifest")}>
94
95
  <RefreshCw size={14} className={isLoading ? "animate-spin" : ""} />
95
96
  </Button>
96
97
  }
@@ -132,7 +133,7 @@ export function DeckScreen() {
132
133
 
133
134
  {/* Active project + stats (read-only context) */}
134
135
  {data?.apx && (
135
- <Section title="Contexto APX" description="Información que el Deck ve del daemon.">
136
+ <Section title={t("deck_screen.context_title")} description="Información que el Deck ve del daemon.">
136
137
  <div className="space-y-2 text-sm" data-testid="deck-apx-context">
137
138
  <div className="flex items-center gap-2">
138
139
  <span className="text-muted-fg">Proyecto activo:</span>
@@ -7,6 +7,7 @@ import { UiSelect } from "../../components/UiSelect";
7
7
  import { useToast } from "../../components/Toast";
8
8
  import { useGlobalConfig } from "../../hooks/useGlobalConfig";
9
9
  import { Desktop, fetchDesktopMessages, type GlobalMessage } from "../../lib/api/desktop";
10
+ import { t } from "../../i18n";
10
11
 
11
12
  const DEFAULT_SHORTCUT = "CommandOrControl+G";
12
13
  const POSITION_OPTS = [
@@ -100,7 +101,7 @@ export function DesktopScreen() {
100
101
  <div className="grid gap-6 xl:grid-cols-[1fr_1fr]">
101
102
  {/* ── LEFT: configuration + status ─────────────────────────────── */}
102
103
  <div className="space-y-6">
103
- <Section title="Estado" description="La ventana se lanza desde la terminal o por autostart.">
104
+ <Section title={t("desktop_screen.status_title")} description="La ventana se lanza desde la terminal o por autostart.">
104
105
  {stLoading ? <Loading /> : (
105
106
  <div className="flex items-center gap-2 text-sm">
106
107
  <StatusDot ok={running} />
@@ -120,7 +121,7 @@ export function DesktopScreen() {
120
121
  </Section>
121
122
 
122
123
  <Section
123
- title="Arranque automático"
124
+ title={t("desktop_screen.autostart_title")}
124
125
  description="Lanza la ventana al iniciar sesión del usuario. Equivalente a `apx desktop install` (no requiere sudo)."
125
126
  >
126
127
  {!autostart ? <Loading /> : (
@@ -137,7 +138,7 @@ export function DesktopScreen() {
137
138
  </Section>
138
139
 
139
140
  <Section
140
- title="Atajo de teclado"
141
+ title={t("desktop_screen.shortcut_title")}
141
142
  description="Botón de acceso rápido global que muestra/oculta la ventana y arranca a escuchar."
142
143
  >
143
144
  {cfgLoading ? <Loading /> : (
@@ -169,7 +170,7 @@ export function DesktopScreen() {
169
170
  )}
170
171
  </Section>
171
172
 
172
- <Section title="Apariencia" description="Tema y posición de la ventana en la pantalla.">
173
+ <Section title={t("desktop_screen.appearance_title")} description="Tema y posición de la ventana en la pantalla.">
173
174
  {cfgLoading ? <Loading /> : (
174
175
  <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
175
176
  <Field label="Tema" hint="Reiniciá la ventana para aplicar.">
@@ -193,7 +194,7 @@ export function DesktopScreen() {
193
194
  </Section>
194
195
 
195
196
  <Section
196
- title="Activación + transcripción"
197
+ title={t("desktop_screen.activation_title")}
197
198
  description="El plugin del daemon procesa los mensajes. STT se configura en Voces."
198
199
  >
199
200
  {cfgLoading ? <Loading /> : (
@@ -216,7 +217,7 @@ export function DesktopScreen() {
216
217
  {/* ── RIGHT: last conversation preview ─────────────────────────── */}
217
218
  <div>
218
219
  <Section
219
- title="Última conversación"
220
+ title={t("desktop_screen.last_conv_title")}
220
221
  description="Lo último charlado con el agente desde la ventana flotante."
221
222
  action={
222
223
  <button
@@ -9,6 +9,7 @@ import { VoiceProviderModal, type VoiceProviderSave } from "../../components/voi
9
9
  import { VoiceTestCard } from "../../components/voice/VoiceTestCard";
10
10
  import { VoiceSttCard } from "../../components/voice/VoiceSttCard";
11
11
  import { Voice, type TranscriptionConfig, type TtsMode, type VoiceTtsConfig } from "../../lib/api/voice";
12
+ import { t } from "../../i18n";
12
13
 
13
14
  // Voices module — configure TTS/STT providers, pick the default engine, and
14
15
  // test playback. TTS provider availability comes from the daemon
@@ -123,7 +124,7 @@ export function VoiceScreen() {
123
124
  <div className="grid gap-6 xl:grid-cols-2">
124
125
  {/* Left: TTS providers */}
125
126
  <Section
126
- title="Proveedores de voz (TTS)"
127
+ title={t("voice_screen.providers_title")}
127
128
  description="Motores de síntesis. El estado lo reporta el daemon en vivo. Elegí cuál usar por defecto."
128
129
  >
129
130
  {provLoading || cfgLoading ? (
@@ -148,12 +149,12 @@ export function VoiceScreen() {
148
149
 
149
150
  {/* Right: test + STT */}
150
151
  <div className="space-y-6">
151
- <Section title="Probar voz" description='Elegí con qué motor sintetizar y, si aplica, cómo querés que hable.'>
152
+ <Section title={t("voice_screen.test_title")} description='Elegí con qué motor sintetizar y, si aplica, cómo querés que hable.'>
152
153
  <VoiceTestCard engines={engines} defaultProvider={configuredProvider} mode={mode} />
153
154
  </Section>
154
155
 
155
156
  <Section
156
- title="Transcripción (STT)"
157
+ title={t("voice_screen.stt_title")}
157
158
  description="Motor de voz a texto que usan el deck, Telegram y la CLI al escuchar."
158
159
  >
159
160
  {cfgLoading ? <Loading /> : <VoiceSttCard config={transcriptionCfg} onPatch={patchStt} />}
@@ -128,7 +128,7 @@ export function AgentDetailScreen({ pid }: { pid: string }) {
128
128
  <Stat label="Heartbeats" value={myRoutines.length} icon={Heart} />
129
129
  </div>
130
130
  <div className="grid gap-3 sm:grid-cols-2">
131
- <Section title="Skills & tools" description="">
131
+ <Section title={t("agent_detail_extra.skills_title")} description="">
132
132
  <div className="flex flex-wrap gap-1">
133
133
  {a.skills?.map((s) => <Badge key={s} tone="info"><Sparkles size={10} /> {s}</Badge>)}
134
134
  {a.tools?.map((t) => <Badge key={t}><Wrench size={10} /> {t}</Badge>)}
@@ -1,18 +1,25 @@
1
+ import { useState } from "react";
2
+ import { useNavigate } from "react-router-dom";
1
3
  import useSWR from "swr";
4
+ import { RefreshCw, Trash2 } from "lucide-react";
2
5
  import { Projects } from "../../lib/api";
3
6
  import { Section } from "../../components/Section";
4
- import { Empty, Loading } from "../../components/ui";
7
+ import { Button, Dialog, Empty, Loading } from "../../components/ui";
5
8
  import { Tabs, TabsContent, TabsList, TabsTrigger } from "../../components/ui/tabs";
6
9
  import { ConfigTabsEditor } from "../../components/config/ConfigTabsEditor";
7
10
  import { APC_PROJECT_SECTIONS, PROJECT_OVERRIDE_SECTIONS } from "../../components/config/project-config-sections";
8
11
  import { useToast } from "../../components/Toast";
12
+ import { useProject } from "../../hooks/useProjects";
9
13
  import { flattenObject } from "../../lib/config-values";
10
14
  import { isSecretMarker } from "../../lib/secrets";
11
15
  import { t } from "../../i18n";
12
16
 
13
17
  export function ConfigTab({ pid }: { pid: string }) {
14
18
  const toast = useToast();
19
+ const navigate = useNavigate();
20
+ const { project, mutate: mutateProject } = useProject(pid);
15
21
  const cfg = useSWR(`/projects/${pid}/config`, () => Projects.config.show(pid));
22
+ const isBase = String(pid) === "0";
16
23
 
17
24
  if (cfg.isLoading) return <Loading />;
18
25
  if (!cfg.data) return <Empty>{t("project.config.no_data")}</Empty>;
@@ -81,10 +88,134 @@ export function ConfigTab({ pid }: { pid: string }) {
81
88
  </TabsContent>
82
89
  </Tabs>
83
90
  </Section>
91
+
92
+ {!isBase && project ? (
93
+ <DangerZone
94
+ pid={pid}
95
+ label={project.name || project.path}
96
+ onRebuilt={() => cfg.mutate()}
97
+ onUnregistered={() => { mutateProject(); navigate("/"); }}
98
+ />
99
+ ) : null}
84
100
  </div>
85
101
  );
86
102
  }
87
103
 
104
+ function DangerZone({
105
+ pid,
106
+ label,
107
+ onRebuilt,
108
+ onUnregistered,
109
+ }: {
110
+ pid: string;
111
+ label: string;
112
+ onRebuilt: () => void;
113
+ onUnregistered: () => void;
114
+ }) {
115
+ const toast = useToast();
116
+ const [busy, setBusy] = useState<"rebuild" | "unregister" | null>(null);
117
+ const [confirm, setConfirm] = useState<"rebuild" | "unregister" | null>(null);
118
+
119
+ const runRebuild = async () => {
120
+ setBusy("rebuild");
121
+ try {
122
+ await Projects.rebuild(pid);
123
+ toast.success(t("project.rebuild_done"));
124
+ onRebuilt();
125
+ } catch (e) {
126
+ toast.error((e as Error).message);
127
+ } finally {
128
+ setBusy(null);
129
+ setConfirm(null);
130
+ }
131
+ };
132
+
133
+ const runUnregister = async () => {
134
+ setBusy("unregister");
135
+ try {
136
+ await Projects.remove(pid);
137
+ toast.success(t("project.unregistered"));
138
+ onUnregistered();
139
+ } catch (e) {
140
+ toast.error((e as Error).message);
141
+ } finally {
142
+ setBusy(null);
143
+ setConfirm(null);
144
+ }
145
+ };
146
+
147
+ return (
148
+ <>
149
+ <Section
150
+ title={t("project.danger.title")}
151
+ description={t("project.danger.subtitle")}
152
+ >
153
+ <div className="space-y-3">
154
+ <div className="flex items-start justify-between gap-3 rounded-md border border-border bg-muted/30 px-3 py-3">
155
+ <div className="min-w-0">
156
+ <div className="text-sm font-medium">{t("project.rebuild")}</div>
157
+ <div className="text-xs text-muted-fg">{t("project.danger.rebuild_desc")}</div>
158
+ </div>
159
+ <Button size="sm" variant="secondary" onClick={() => setConfirm("rebuild")}>
160
+ <RefreshCw size={13} /> {t("project.rebuild")}
161
+ </Button>
162
+ </div>
163
+
164
+ <div className="flex items-start justify-between gap-3 rounded-md border border-red-500/40 bg-red-500/5 px-3 py-3">
165
+ <div className="min-w-0">
166
+ <div className="text-sm font-medium">{t("admin.unregister")}</div>
167
+ <div className="text-xs text-muted-fg">{t("project.danger.unregister_desc")}</div>
168
+ </div>
169
+ <Button size="sm" variant="destructive" onClick={() => setConfirm("unregister")}>
170
+ <Trash2 size={13} /> {t("admin.unregister")}
171
+ </Button>
172
+ </div>
173
+ </div>
174
+ </Section>
175
+
176
+ <Dialog
177
+ open={confirm === "rebuild"}
178
+ onClose={() => (busy ? null : setConfirm(null))}
179
+ title={t("project.danger.rebuild_confirm_title")}
180
+ description={t("project.danger.rebuild_confirm_desc", { label })}
181
+ size="sm"
182
+ footer={
183
+ <>
184
+ <Button variant="ghost" onClick={() => setConfirm(null)} disabled={busy !== null}>
185
+ {t("common.cancel")}
186
+ </Button>
187
+ <Button variant="primary" onClick={runRebuild} loading={busy === "rebuild"}>
188
+ {t("project.rebuild")}
189
+ </Button>
190
+ </>
191
+ }
192
+ >
193
+ <p className="text-sm text-muted-fg">{t("project.danger.rebuild_long")}</p>
194
+ </Dialog>
195
+
196
+ <Dialog
197
+ open={confirm === "unregister"}
198
+ onClose={() => (busy ? null : setConfirm(null))}
199
+ title={t("project.danger.unregister_confirm_title")}
200
+ description={t("project.unregister_confirm", { label })}
201
+ size="sm"
202
+ footer={
203
+ <>
204
+ <Button variant="ghost" onClick={() => setConfirm(null)} disabled={busy !== null}>
205
+ {t("common.cancel")}
206
+ </Button>
207
+ <Button variant="destructive" onClick={runUnregister} loading={busy === "unregister"}>
208
+ {t("admin.unregister")}
209
+ </Button>
210
+ </>
211
+ }
212
+ >
213
+ <p className="text-sm text-muted-fg">{t("project.danger.unregister_long")}</p>
214
+ </Dialog>
215
+ </>
216
+ );
217
+ }
218
+
88
219
  function cleanSet(set: Record<string, unknown>) {
89
220
  const out: Record<string, unknown> = {};
90
221
  for (const [key, value] of Object.entries(flattenObject(set))) {