@agentprojectcontext/apx 1.37.0 → 1.38.1
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.
- package/README.md +11 -0
- package/package.json +1 -1
- package/src/core/mascot.js +80 -80
- package/src/interfaces/web/dist/assets/index-CQc_5t8F.js +629 -0
- package/src/interfaces/web/dist/assets/index-CQc_5t8F.js.map +1 -0
- package/src/interfaces/web/dist/assets/{index-B6sYFQFa.css → index-hwxuTPcK.css} +1 -1
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/src/App.tsx +15 -24
- package/src/interfaces/web/src/components/ModelCombobox.tsx +1 -1
- package/src/interfaces/web/src/components/Roby.tsx +96 -0
- package/src/interfaces/web/src/components/TelegramChannelDialog.tsx +11 -11
- package/src/interfaces/web/src/components/TelegramSendDialog.tsx +5 -5
- package/src/interfaces/web/src/components/chat/MessageBubble.tsx +2 -2
- package/src/interfaces/web/src/components/chat/ModelPicker.tsx +5 -5
- package/src/interfaces/web/src/components/chat/ToolCall.tsx +23 -19
- package/src/interfaces/web/src/components/code/CodeArtifactsTab.tsx +10 -10
- package/src/interfaces/web/src/components/code/CodeContextTab.tsx +7 -7
- package/src/interfaces/web/src/components/code/CodeProjectPicker.tsx +3 -2
- package/src/interfaces/web/src/components/common/TabNav.tsx +3 -2
- package/src/interfaces/web/src/components/config/ConfigTabsEditor.tsx +3 -2
- package/src/interfaces/web/src/components/config/GlobalConfigEditor.tsx +2 -2
- package/src/interfaces/web/src/components/config/global-config-sections.ts +9 -9
- package/src/interfaces/web/src/components/config/project-config-sections.ts +61 -54
- package/src/interfaces/web/src/components/deck/DaemonCard.tsx +6 -5
- package/src/interfaces/web/src/components/inputs/KeyValueList.tsx +5 -4
- package/src/interfaces/web/src/components/inputs/VarTokenInput.tsx +3 -3
- package/src/interfaces/web/src/components/layout/ProjectSidebar.tsx +22 -9
- package/src/interfaces/web/src/components/settings/AdvancedPanel.tsx +1 -1
- package/src/interfaces/web/src/components/settings/AppearancePanel.tsx +1 -1
- package/src/interfaces/web/src/components/settings/DefaultRouterCard.tsx +14 -14
- package/src/interfaces/web/src/components/settings/DevicesPanel.tsx +3 -3
- package/src/interfaces/web/src/components/settings/EnginesPanel.tsx +7 -7
- package/src/interfaces/web/src/components/settings/IdentityPanel.tsx +2 -2
- package/src/interfaces/web/src/components/settings/MemoryPanel.tsx +37 -37
- package/src/interfaces/web/src/components/settings/SkillsInspectorPanel.tsx +44 -35
- package/src/interfaces/web/src/components/settings/SuperAgentPanel.tsx +5 -5
- package/src/interfaces/web/src/components/settings/TelegramChannelsPanel.tsx +3 -3
- package/src/interfaces/web/src/components/settings/TelegramContactsPanel.tsx +1 -1
- package/src/interfaces/web/src/components/settings/TelegramGlobalPanel.tsx +3 -3
- package/src/interfaces/web/src/components/settings/TelegramRolesPanel.tsx +1 -1
- package/src/interfaces/web/src/components/settings/providers/ProviderCard.tsx +6 -6
- package/src/interfaces/web/src/components/settings/providers/ProviderModal.tsx +36 -36
- package/src/interfaces/web/src/components/voice/VoiceProviderList.tsx +15 -14
- package/src/interfaces/web/src/components/voice/VoiceProviderModal.tsx +22 -22
- package/src/interfaces/web/src/components/voice/VoiceSttCard.tsx +18 -17
- package/src/interfaces/web/src/components/voice/VoiceTestCard.tsx +19 -18
- package/src/interfaces/web/src/hooks/useChat.ts +6 -5
- package/src/interfaces/web/src/i18n/en.ts +517 -1
- package/src/interfaces/web/src/i18n/es.ts +517 -1
- package/src/interfaces/web/src/i18n/index.ts +1 -1
- package/src/interfaces/web/src/lib/api/voice.ts +5 -5
- package/src/interfaces/web/src/screens/ProjectScreen.tsx +14 -1
- package/src/interfaces/web/src/screens/SettingsScreen.tsx +1 -1
- package/src/interfaces/web/src/screens/base/AgentDefaultsTab.tsx +8 -8
- package/src/interfaces/web/src/screens/base/ComingSoon.tsx +3 -2
- package/src/interfaces/web/src/screens/modules/CodeScreen.tsx +12 -12
- package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +15 -15
- package/src/interfaces/web/src/screens/modules/DesktopScreen.tsx +37 -37
- package/src/interfaces/web/src/screens/modules/VoiceScreen.tsx +8 -8
- package/src/interfaces/web/src/screens/project/AgentBrainGraph.tsx +16 -10
- package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +25 -24
- package/src/interfaces/web/src/screens/project/ChatTab.tsx +2 -2
- package/src/interfaces/web/src/screens/project/ConfigTab.tsx +3 -3
- package/src/interfaces/web/src/screens/project/McpsTab.tsx +6 -9
- package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +66 -52
- package/src/interfaces/web/src/screens/project/TelegramTab.tsx +1 -1
- package/src/interfaces/web/dist/assets/index-DsADpObh.js +0 -633
- package/src/interfaces/web/dist/assets/index-DsADpObh.js.map +0 -1
|
@@ -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-
|
|
22
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
21
|
+
<script type="module" crossorigin src="/assets/index-CQc_5t8F.js"></script>
|
|
22
|
+
<link rel="stylesheet" crossorigin href="/assets/index-hwxuTPcK.css">
|
|
23
23
|
</head>
|
|
24
24
|
<body class="bg-background text-foreground antialiased">
|
|
25
25
|
<div id="root"></div>
|
|
@@ -12,6 +12,7 @@ import { CodeScreen } from "./screens/modules/CodeScreen";
|
|
|
12
12
|
import { AddProjectDialog } from "./components/AddProjectDialog";
|
|
13
13
|
import { PairingScreen } from "./screens/PairingScreen";
|
|
14
14
|
import { RobyBubble } from "./components/RobyBubble";
|
|
15
|
+
import { Roby, RobyEmpty, type RobyMood } from "./components/Roby";
|
|
15
16
|
import { ToastProvider } from "./components/Toast";
|
|
16
17
|
import { Button } from "./components/ui/button";
|
|
17
18
|
import { TooltipProvider } from "./components/ui/tooltip";
|
|
@@ -31,6 +32,7 @@ export function App() {
|
|
|
31
32
|
if (auth.status === "error") {
|
|
32
33
|
return (
|
|
33
34
|
<Splash
|
|
35
|
+
mood="sad"
|
|
34
36
|
text={t("daemon.unreachable")}
|
|
35
37
|
sub={`${t("daemon.unreachable_hint")}\n\n${auth.reason}`}
|
|
36
38
|
/>
|
|
@@ -197,13 +199,11 @@ function projectLabel(key?: string) {
|
|
|
197
199
|
}
|
|
198
200
|
}
|
|
199
201
|
|
|
200
|
-
function Splash({ text, sub }: { text: string; sub?: string }) {
|
|
202
|
+
function Splash({ text, sub, mood = "happy" }: { text: string; sub?: string; mood?: RobyMood }) {
|
|
201
203
|
return (
|
|
202
204
|
<div className="grid h-screen w-screen place-items-center bg-background text-foreground">
|
|
203
|
-
<div className="text-center">
|
|
204
|
-
<
|
|
205
|
-
{" ▄███████▄\n █ ██ ██ █\n █ ◔ ◔ █\n █ ╰~╯ █\n ▀███████▀"}
|
|
206
|
-
</div>
|
|
205
|
+
<div className="flex flex-col items-center text-center">
|
|
206
|
+
<Roby mood={mood} className="mb-4 text-xs" />
|
|
207
207
|
<div className="text-foreground">{text}</div>
|
|
208
208
|
{sub && <pre className="mt-2 max-w-xl whitespace-pre-wrap text-sm text-muted-fg">{sub}</pre>}
|
|
209
209
|
</div>
|
|
@@ -213,27 +213,18 @@ function Splash({ text, sub }: { text: string; sub?: string }) {
|
|
|
213
213
|
|
|
214
214
|
function NotFound() {
|
|
215
215
|
const navigate = useNavigate();
|
|
216
|
-
// Roby, but lost — a confused riff on the Splash mascot (asymmetric eyes,
|
|
217
|
-
// little "o" mouth, a floating "?"). Tinted with the APX brand green.
|
|
218
|
-
const robyLost =
|
|
219
|
-
" ?\n ▄███████▄\n █ ██ ██ █\n █ ◑ ◐ █\n █ o █\n ▀███████▀";
|
|
220
216
|
return (
|
|
221
|
-
<
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
<div className="font-mono text-7xl font-semibold leading-none tracking-tight text-foreground">
|
|
230
|
-
{t("not_found.title")}
|
|
231
|
-
</div>
|
|
232
|
-
<p className="mt-4 max-w-sm text-sm text-muted-fg">{t("not_found.message")}</p>
|
|
233
|
-
<Button variant="outline" className="mt-6" onClick={() => navigate("/")}>
|
|
217
|
+
<RobyEmpty
|
|
218
|
+
testId="screen-not-found"
|
|
219
|
+
mood="confused"
|
|
220
|
+
title={t("not_found.title")}
|
|
221
|
+
titleClassName="text-7xl"
|
|
222
|
+
message={t("not_found.message")}
|
|
223
|
+
action={
|
|
224
|
+
<Button variant="outline" onClick={() => navigate("/")}>
|
|
234
225
|
{t("not_found.home")}
|
|
235
226
|
</Button>
|
|
236
|
-
|
|
237
|
-
|
|
227
|
+
}
|
|
228
|
+
/>
|
|
238
229
|
);
|
|
239
230
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { cn } from "@/lib/utils";
|
|
3
|
+
|
|
4
|
+
// Roby — the APX mascot. Web mirror of the CLI version in src/core/mascot.js:
|
|
5
|
+
// a chunky ▄███████▄ head with two screen-eyes and a tiny mouth, drawn as clean
|
|
6
|
+
// emerald line art. Keep the FACES table below in sync with the CLI's MOODS so
|
|
7
|
+
// the same character shows up identically across terminal and web.
|
|
8
|
+
export type RobyMood = "happy" | "wave" | "confused" | "sad" | "excited" | "sleeping";
|
|
9
|
+
|
|
10
|
+
// Each mood = a pair of pupils, a mouth glyph, and an optional floating accent.
|
|
11
|
+
// NB: the sad mouth is "◠" (single-cell) rather than the CLI's "︵": that glyph
|
|
12
|
+
// is East-Asian-wide (~1.7 cells) in the browser monospace font and would push
|
|
13
|
+
// the head's right edge out of line. "◠" is the single-width frown inverse of
|
|
14
|
+
// the happy "‿".
|
|
15
|
+
const FACES: Record<RobyMood, { eyes: [string, string]; mouth: string; top?: string }> = {
|
|
16
|
+
happy: { eyes: ["◕", "◕"], mouth: "‿" },
|
|
17
|
+
wave: { eyes: ["◕", "◕"], mouth: "▽", top: "·" },
|
|
18
|
+
confused: { eyes: ["◑", "◐"], mouth: "o", top: "?" },
|
|
19
|
+
sad: { eyes: ["╥", "╥"], mouth: "◠" },
|
|
20
|
+
excited: { eyes: ["★", "★"], mouth: "▽", top: "✦" },
|
|
21
|
+
sleeping: { eyes: ["−", "−"], mouth: "‿", top: "z z" },
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export function Roby({ mood = "happy", className }: { mood?: RobyMood; className?: string }) {
|
|
25
|
+
const face = FACES[mood] ?? FACES.happy;
|
|
26
|
+
const [el, er] = face.eyes;
|
|
27
|
+
// Recessed eye "screens" + floating accent sit in a dimmer emerald; the bright
|
|
28
|
+
// frame, pupils and mouth inherit text-emerald-400 from the wrapper.
|
|
29
|
+
const dim = "text-emerald-700 dark:text-emerald-600/70";
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
aria-hidden
|
|
33
|
+
className={cn(
|
|
34
|
+
"select-none whitespace-pre font-mono leading-none text-emerald-400",
|
|
35
|
+
className
|
|
36
|
+
)}
|
|
37
|
+
>
|
|
38
|
+
{face.top && (
|
|
39
|
+
<div>
|
|
40
|
+
<span className={dim}>{` ${face.top}`}</span>
|
|
41
|
+
</div>
|
|
42
|
+
)}
|
|
43
|
+
<div>{" ▄███████▄"}</div>
|
|
44
|
+
<div>
|
|
45
|
+
{" █ "}
|
|
46
|
+
<span className={dim}>{"██"}</span>
|
|
47
|
+
{" "}
|
|
48
|
+
<span className={dim}>{"██"}</span>
|
|
49
|
+
{" █"}
|
|
50
|
+
</div>
|
|
51
|
+
<div>{` █ ${el} ${er} █`}</div>
|
|
52
|
+
<div>{` █ ${face.mouth} █`}</div>
|
|
53
|
+
<div>{" ▀███████▀"}</div>
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Centered "Roby + message" layout shared by the 404 and the project-not-found
|
|
59
|
+
// screens. `title` is optional and rendered large (e.g. a giant "404").
|
|
60
|
+
export function RobyEmpty({
|
|
61
|
+
mood = "confused",
|
|
62
|
+
title,
|
|
63
|
+
titleClassName,
|
|
64
|
+
message,
|
|
65
|
+
action,
|
|
66
|
+
className,
|
|
67
|
+
testId,
|
|
68
|
+
}: {
|
|
69
|
+
mood?: RobyMood;
|
|
70
|
+
title?: ReactNode;
|
|
71
|
+
titleClassName?: string;
|
|
72
|
+
message?: ReactNode;
|
|
73
|
+
action?: ReactNode;
|
|
74
|
+
className?: string;
|
|
75
|
+
testId?: string;
|
|
76
|
+
}) {
|
|
77
|
+
return (
|
|
78
|
+
<div className={cn("grid h-full place-items-center p-8", className)} data-testid={testId}>
|
|
79
|
+
<div className="flex flex-col items-center text-center">
|
|
80
|
+
<Roby mood={mood} className="mb-6 text-sm" />
|
|
81
|
+
{title != null && (
|
|
82
|
+
<div
|
|
83
|
+
className={cn(
|
|
84
|
+
"font-mono font-semibold leading-none tracking-tight text-foreground",
|
|
85
|
+
titleClassName
|
|
86
|
+
)}
|
|
87
|
+
>
|
|
88
|
+
{title}
|
|
89
|
+
</div>
|
|
90
|
+
)}
|
|
91
|
+
{message != null && <p className="mt-4 max-w-sm text-sm text-muted-fg">{message}</p>}
|
|
92
|
+
{action && <div className="mt-6">{action}</div>}
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
@@ -23,7 +23,7 @@ export function TelegramChannelDialog({ channel, onClose, onSaved }: Props) {
|
|
|
23
23
|
}, [channel?.name]);
|
|
24
24
|
|
|
25
25
|
const submit = async () => {
|
|
26
|
-
if (!draft.name?.trim()) { toast.error("
|
|
26
|
+
if (!draft.name?.trim()) { toast.error(t("telegram_channel_dialog.name_required")); return; }
|
|
27
27
|
setBusy(true);
|
|
28
28
|
try {
|
|
29
29
|
const isExisting = channel && channel.name !== "";
|
|
@@ -32,7 +32,7 @@ export function TelegramChannelDialog({ channel, onClose, onSaved }: Props) {
|
|
|
32
32
|
} else {
|
|
33
33
|
await Telegram.channels.upsert(draft);
|
|
34
34
|
}
|
|
35
|
-
toast.success("
|
|
35
|
+
toast.success(t("telegram_channel_dialog.saved"));
|
|
36
36
|
onSaved();
|
|
37
37
|
} catch (e) {
|
|
38
38
|
toast.error((e as Error).message);
|
|
@@ -44,7 +44,7 @@ export function TelegramChannelDialog({ channel, onClose, onSaved }: Props) {
|
|
|
44
44
|
open={!!channel}
|
|
45
45
|
onClose={onClose}
|
|
46
46
|
title={channel?.name ? t("telegram_channel_dialog.edit_title", { name: channel.name }) : t("telegram_channel_dialog.new_title")}
|
|
47
|
-
description="
|
|
47
|
+
description={t("telegram_ui.channel_dialog_desc")}
|
|
48
48
|
footer={
|
|
49
49
|
<>
|
|
50
50
|
<Button variant="ghost" onClick={onClose} disabled={busy}>{t("common.cancel")}</Button>
|
|
@@ -53,10 +53,10 @@ export function TelegramChannelDialog({ channel, onClose, onSaved }: Props) {
|
|
|
53
53
|
}
|
|
54
54
|
>
|
|
55
55
|
<div className="space-y-3">
|
|
56
|
-
<Field label="
|
|
56
|
+
<Field label={t("telegram_channel_dialog.name_label")}>
|
|
57
57
|
<Input value={draft.name} onChange={(e) => setDraft({ ...draft, name: e.target.value })} disabled={!!channel?.name} />
|
|
58
58
|
</Field>
|
|
59
|
-
<Field label="
|
|
59
|
+
<Field label={t("telegram_channel_dialog.token_label")} hint={channel?.bot_token ? secretHint(channel.bot_token) : t("telegram_ui.bot_token_hint")}>
|
|
60
60
|
<Input
|
|
61
61
|
type="password"
|
|
62
62
|
value={draft.bot_token || ""}
|
|
@@ -64,18 +64,18 @@ export function TelegramChannelDialog({ channel, onClose, onSaved }: Props) {
|
|
|
64
64
|
placeholder={channel?.bot_token ? secretHint(channel.bot_token) : ""}
|
|
65
65
|
/>
|
|
66
66
|
</Field>
|
|
67
|
-
<Field label="chat_id">
|
|
67
|
+
<Field label={t("telegram_channel_dialog.chat_id")}>
|
|
68
68
|
<Input value={draft.chat_id || ""} onChange={(e) => setDraft({ ...draft, chat_id: e.target.value })} />
|
|
69
69
|
</Field>
|
|
70
|
-
<Field label="
|
|
70
|
+
<Field label={t("telegram_channel_dialog.project_label")} hint={t("telegram_channel_dialog.project_hint")}>
|
|
71
71
|
<Input value={draft.project || ""} onChange={(e) => setDraft({ ...draft, project: e.target.value })} />
|
|
72
72
|
</Field>
|
|
73
|
-
<Field label="
|
|
73
|
+
<Field label={t("telegram_channel_dialog.route_label")} hint={t("telegram_channel_dialog.route_hint")}>
|
|
74
74
|
<Input value={draft.route_to_agent || ""} onChange={(e) => setDraft({ ...draft, route_to_agent: e.target.value })} />
|
|
75
75
|
</Field>
|
|
76
76
|
<Field
|
|
77
|
-
label="
|
|
78
|
-
hint="
|
|
77
|
+
label={t("telegram_channel_dialog.owner_label")}
|
|
78
|
+
hint={t("telegram_channel_dialog.owner_hint")}
|
|
79
79
|
>
|
|
80
80
|
<Input
|
|
81
81
|
value={draft.owner_user_id != null ? String(draft.owner_user_id) : ""}
|
|
@@ -89,7 +89,7 @@ export function TelegramChannelDialog({ channel, onClose, onSaved }: Props) {
|
|
|
89
89
|
<Switch
|
|
90
90
|
checked={!!draft.respond_with_engine}
|
|
91
91
|
onChange={(v) => setDraft({ ...draft, respond_with_engine: v })}
|
|
92
|
-
label="
|
|
92
|
+
label={t("telegram_channel_dialog.respond_label")}
|
|
93
93
|
/>
|
|
94
94
|
</div>
|
|
95
95
|
</Dialog>
|
|
@@ -20,7 +20,7 @@ export function TelegramSendDialog({ channel, onClose }: Props) {
|
|
|
20
20
|
setBusy(true);
|
|
21
21
|
try {
|
|
22
22
|
await Telegram.send({ text, channel: channel.name });
|
|
23
|
-
toast.success("
|
|
23
|
+
toast.success(t("telegram_ui.message_sent"));
|
|
24
24
|
onClose();
|
|
25
25
|
} catch (e) {
|
|
26
26
|
toast.error((e as Error).message);
|
|
@@ -31,16 +31,16 @@ export function TelegramSendDialog({ channel, onClose }: Props) {
|
|
|
31
31
|
<Dialog
|
|
32
32
|
open={!!channel}
|
|
33
33
|
onClose={onClose}
|
|
34
|
-
title={channel ?
|
|
35
|
-
description={channel ?
|
|
34
|
+
title={channel ? t("telegram_send_dialog.title", { name: channel.name }) : ""}
|
|
35
|
+
description={channel ? t("telegram_ui.send_chat_id", { id: channel.chat_id || "—" }) : ""}
|
|
36
36
|
footer={
|
|
37
37
|
<>
|
|
38
38
|
<Button variant="ghost" onClick={onClose} disabled={busy}>{t("common.cancel")}</Button>
|
|
39
|
-
<Button variant="primary" onClick={submit} loading={busy}>
|
|
39
|
+
<Button variant="primary" onClick={submit} loading={busy}>{t("chat_ui.send")}</Button>
|
|
40
40
|
</>
|
|
41
41
|
}
|
|
42
42
|
>
|
|
43
|
-
<Field label="
|
|
43
|
+
<Field label={t("telegram_ui.message_label")}>
|
|
44
44
|
<Textarea rows={4} value={text} onChange={(e) => setText(e.target.value)} />
|
|
45
45
|
</Field>
|
|
46
46
|
</Dialog>
|
|
@@ -53,7 +53,7 @@ export function MessageBubble({ msg, isLast, isAskAnswer, onCopy }: Props) {
|
|
|
53
53
|
{!mine && msg.inspector && (msg.inspector.loaded?.length || msg.inspector.hinted?.length) ? (
|
|
54
54
|
<div
|
|
55
55
|
className="flex flex-wrap items-center gap-1 text-[10px] text-sky-400/90"
|
|
56
|
-
title={
|
|
56
|
+
title={t("shared_ui.skill_inspector_title", { embedder: msg.inspector.embedder || "RAG" })}
|
|
57
57
|
>
|
|
58
58
|
<Sparkles size={10} />
|
|
59
59
|
{msg.inspector.loaded?.map((s) => (
|
|
@@ -107,7 +107,7 @@ export function MessageBubble({ msg, isLast, isAskAnswer, onCopy }: Props) {
|
|
|
107
107
|
<span>{formatTs(msg.ts)}</span>
|
|
108
108
|
{!mine && msg.model && <span className="font-mono">· {msg.model}</span>}
|
|
109
109
|
{!mine && hasTools && (
|
|
110
|
-
<span>· {msg.parts.filter((p) => p.kind === "tool").length}
|
|
110
|
+
<span>· {t("shared_ui.tools_count", { n: msg.parts.filter((p) => p.kind === "tool").length })}</span>
|
|
111
111
|
)}
|
|
112
112
|
{onCopy && copyText && (
|
|
113
113
|
<button
|
|
@@ -64,7 +64,7 @@ export function ModelPicker({
|
|
|
64
64
|
|
|
65
65
|
const q = query.trim().toLowerCase();
|
|
66
66
|
const filtered = q ? options.filter((o) => o.toLowerCase().includes(q)) : options;
|
|
67
|
-
const label = value || "
|
|
67
|
+
const label = value || t("shared_ui.auto");
|
|
68
68
|
|
|
69
69
|
const pick = (m: string) => { onChange(m); setOpen(false); setQuery(""); };
|
|
70
70
|
|
|
@@ -93,7 +93,7 @@ export function ModelPicker({
|
|
|
93
93
|
<input
|
|
94
94
|
autoFocus
|
|
95
95
|
value={query}
|
|
96
|
-
placeholder="
|
|
96
|
+
placeholder={t("shared_ui.model_filter_ph")}
|
|
97
97
|
onChange={(e) => setQuery(e.target.value)}
|
|
98
98
|
onKeyDown={(e) => { if (e.key === "Enter" && query.trim()) pick(query.trim()); }}
|
|
99
99
|
className="mb-1 w-full rounded-md border border-border bg-background px-2 py-1 text-xs outline-none focus:border-foreground/30"
|
|
@@ -108,11 +108,11 @@ export function ModelPicker({
|
|
|
108
108
|
!value && "bg-accent/50",
|
|
109
109
|
)}
|
|
110
110
|
>
|
|
111
|
-
<span className="flex items-center gap-1.5"><X className="size-3" />
|
|
111
|
+
<span className="flex items-center gap-1.5"><X className="size-3" /> {t("shared_ui.auto_router")}</span>
|
|
112
112
|
{!value && <Check className="size-3" />}
|
|
113
113
|
</button>
|
|
114
114
|
</li>
|
|
115
|
-
{!loaded && <li className="px-2 py-1 text-[11px] text-muted-fg">
|
|
115
|
+
{!loaded && <li className="px-2 py-1 text-[11px] text-muted-fg">{t("shared_ui.loading_models")}</li>}
|
|
116
116
|
{loaded && filtered.length === 0 && query.trim() && (
|
|
117
117
|
<li>
|
|
118
118
|
<button
|
|
@@ -120,7 +120,7 @@ export function ModelPicker({
|
|
|
120
120
|
onMouseDown={(e) => { e.preventDefault(); pick(query.trim()); }}
|
|
121
121
|
className="w-full rounded-md px-2 py-1 text-left font-mono text-xs hover:bg-accent hover:text-accent-fg"
|
|
122
122
|
>
|
|
123
|
-
|
|
123
|
+
{t("shared_ui.use_value", { value: query.trim() })}
|
|
124
124
|
</button>
|
|
125
125
|
</li>
|
|
126
126
|
)}
|
|
@@ -19,28 +19,32 @@ import {
|
|
|
19
19
|
} from "lucide-react";
|
|
20
20
|
import { cn } from "../../lib/cn";
|
|
21
21
|
import type { ToolPart } from "../../hooks/useChat";
|
|
22
|
+
import { t } from "../../i18n";
|
|
22
23
|
|
|
23
24
|
// Map registered tool names (core/agent tools) to an icon + friendly label.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
25
|
+
// Built per-call so t() runs against the active locale at render time.
|
|
26
|
+
function toolMeta(): Record<string, { icon: typeof Wrench; label: string }> {
|
|
27
|
+
return {
|
|
28
|
+
read_file: { icon: FileText, label: t("shared_ui.tool_read_file") },
|
|
29
|
+
write_file: { icon: FilePlus, label: t("shared_ui.tool_write_file") },
|
|
30
|
+
edit_file: { icon: FilePen, label: t("shared_ui.tool_edit_file") },
|
|
31
|
+
list_files: { icon: FolderTree, label: t("shared_ui.tool_list_files") },
|
|
32
|
+
search_files: { icon: Search, label: t("shared_ui.tool_search_files") },
|
|
33
|
+
search_messages: { icon: Search, label: t("shared_ui.tool_search_messages") },
|
|
34
|
+
tail_messages: { icon: Search, label: t("shared_ui.tool_tail_messages") },
|
|
35
|
+
run_shell: { icon: Terminal, label: t("shared_ui.tool_run_shell") },
|
|
36
|
+
send_telegram: { icon: Send, label: t("shared_ui.tool_send_telegram") },
|
|
37
|
+
call_agent: { icon: Bot, label: t("shared_ui.tool_call_agent") },
|
|
38
|
+
call_mcp: { icon: Plug, label: t("shared_ui.tool_call_mcp") },
|
|
39
|
+
call_runtime: { icon: Bot, label: t("shared_ui.tool_call_runtime") },
|
|
40
|
+
create_task: { icon: ListTodo, label: t("shared_ui.tool_create_task") },
|
|
41
|
+
};
|
|
42
|
+
}
|
|
39
43
|
|
|
40
44
|
const FILE_TOOLS = new Set(["write_file", "edit_file"]);
|
|
41
45
|
|
|
42
46
|
function metaFor(tool: string) {
|
|
43
|
-
return
|
|
47
|
+
return toolMeta()[tool] || { icon: Wrench, label: tool };
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
// Best-effort one-line argument summary shown next to the tool title.
|
|
@@ -104,7 +108,7 @@ export function ToolCall({ part }: { part: ToolPart }) {
|
|
|
104
108
|
<span className="shrink-0 font-medium">{label}</span>
|
|
105
109
|
{summary && <span className="truncate font-mono text-muted-foreground">{summary}</span>}
|
|
106
110
|
<span className="ml-auto flex items-center gap-1">
|
|
107
|
-
{part.status === "deduped" && <span className="text-[10px] text-amber-400">dedup</span>}
|
|
111
|
+
{part.status === "deduped" && <span className="text-[10px] text-amber-400">{t("shared_ui.dedup")}</span>}
|
|
108
112
|
<StatusIcon status={part.status} />
|
|
109
113
|
</span>
|
|
110
114
|
</button>
|
|
@@ -113,7 +117,7 @@ export function ToolCall({ part }: { part: ToolPart }) {
|
|
|
113
117
|
<div className="space-y-2 border-t border-border/60 px-2.5 py-2">
|
|
114
118
|
{part.args && Object.keys(part.args).length > 0 && (
|
|
115
119
|
<div>
|
|
116
|
-
<div className="mb-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70">args</div>
|
|
120
|
+
<div className="mb-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70">{t("shared_ui.args")}</div>
|
|
117
121
|
<pre className="max-h-48 overflow-auto rounded-md bg-background/60 p-2 font-mono text-[11px] leading-relaxed text-foreground">
|
|
118
122
|
{pretty(part.args)}
|
|
119
123
|
</pre>
|
|
@@ -121,7 +125,7 @@ export function ToolCall({ part }: { part: ToolPart }) {
|
|
|
121
125
|
)}
|
|
122
126
|
{part.result !== undefined && (
|
|
123
127
|
<div>
|
|
124
|
-
<div className="mb-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70">result</div>
|
|
128
|
+
<div className="mb-1 text-[10px] font-semibold uppercase tracking-wide text-muted-foreground/70">{t("shared_ui.result")}</div>
|
|
125
129
|
<pre
|
|
126
130
|
className={cn(
|
|
127
131
|
"max-h-64 overflow-auto rounded-md bg-background/60 p-2 font-mono text-[11px] leading-relaxed",
|
|
@@ -63,7 +63,7 @@ function ArtifactRow({
|
|
|
63
63
|
const copy = async (text: string) => {
|
|
64
64
|
try {
|
|
65
65
|
await navigator.clipboard.writeText(text);
|
|
66
|
-
toast.info("
|
|
66
|
+
toast.info(t("modules_ui.code_copied"));
|
|
67
67
|
} catch {
|
|
68
68
|
/* ignore */
|
|
69
69
|
}
|
|
@@ -75,8 +75,8 @@ function ArtifactRow({
|
|
|
75
75
|
try {
|
|
76
76
|
const r = await Artifacts.run(pid, entry.name);
|
|
77
77
|
setRunResult(r);
|
|
78
|
-
if (r.ok) toast.info(
|
|
79
|
-
else toast.error(
|
|
78
|
+
if (r.ok) toast.info(t("modules_ui.code_artifact_exit_ok", { ms: r.durationMs ?? 0 }));
|
|
79
|
+
else toast.error(t("modules_ui.code_artifact_exit_fail", { code: r.exitCode ?? r.signal ?? "?", timeout: r.timedOut ? t("modules_ui.code_artifact_timeout_suffix") : "" }));
|
|
80
80
|
} catch (e) {
|
|
81
81
|
toast.error((e as Error).message);
|
|
82
82
|
} finally {
|
|
@@ -177,7 +177,7 @@ function ArtifactRow({
|
|
|
177
177
|
className="inline-flex items-center gap-1 rounded px-1.5 py-1 text-[10px] font-medium bg-blue-500/15 text-blue-700 hover:bg-blue-500/25 dark:text-blue-300"
|
|
178
178
|
>
|
|
179
179
|
<Eye className="size-3" />
|
|
180
|
-
|
|
180
|
+
{t("modules_ui.code_artifact_view_short")}
|
|
181
181
|
</button>
|
|
182
182
|
</Tip>
|
|
183
183
|
<DialogContent className="sm:max-w-lg">
|
|
@@ -205,7 +205,7 @@ function ArtifactRow({
|
|
|
205
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
206
|
>
|
|
207
207
|
<SquarePen className="size-3" />
|
|
208
|
-
|
|
208
|
+
{t("modules_ui.code_artifact_edit_short")}
|
|
209
209
|
</button>
|
|
210
210
|
</Tip>
|
|
211
211
|
|
|
@@ -252,7 +252,7 @@ function ArtifactRow({
|
|
|
252
252
|
/>
|
|
253
253
|
}
|
|
254
254
|
>
|
|
255
|
-
|
|
255
|
+
{t("common.cancel")}
|
|
256
256
|
</DialogClose>
|
|
257
257
|
<button
|
|
258
258
|
type="button"
|
|
@@ -266,7 +266,7 @@ function ArtifactRow({
|
|
|
266
266
|
)}
|
|
267
267
|
>
|
|
268
268
|
{deleting && <Spinner size={10} />}
|
|
269
|
-
|
|
269
|
+
{t("code_module.delete")}
|
|
270
270
|
</button>
|
|
271
271
|
</DialogFooter>
|
|
272
272
|
</DialogContent>
|
|
@@ -292,16 +292,16 @@ function ArtifactRow({
|
|
|
292
292
|
: "bg-rose-500/15 text-rose-700 dark:text-rose-300",
|
|
293
293
|
)}
|
|
294
294
|
>
|
|
295
|
-
|
|
295
|
+
{t("modules_ui.code_artifact_exit_badge", { code: runResult.exitCode ?? runResult.signal ?? "?" })}
|
|
296
296
|
</span>
|
|
297
297
|
{runResult.timedOut && (
|
|
298
298
|
<span className="rounded bg-amber-500/15 px-1.5 py-0.5 font-mono text-amber-700 dark:text-amber-300">
|
|
299
|
-
|
|
299
|
+
{t("modules_ui.code_artifact_timeout")}
|
|
300
300
|
</span>
|
|
301
301
|
)}
|
|
302
302
|
{runResult.truncated && (
|
|
303
303
|
<span className="rounded bg-amber-500/15 px-1.5 py-0.5 font-mono text-amber-700 dark:text-amber-300">
|
|
304
|
-
|
|
304
|
+
{t("modules_ui.code_artifact_truncated")}
|
|
305
305
|
</span>
|
|
306
306
|
)}
|
|
307
307
|
<span className="font-mono text-muted-foreground">
|
|
@@ -56,18 +56,18 @@ export function CodeContextTab({ turns, session }: Props) {
|
|
|
56
56
|
|
|
57
57
|
return (
|
|
58
58
|
<div className="space-y-1 p-3" data-testid="code-context-tab">
|
|
59
|
-
<Row label={t("code_module.ctx_model")} value={m.model || "
|
|
60
|
-
{session?.mode && <Row label="
|
|
61
|
-
{session?.agentSlug && <Row label="
|
|
59
|
+
<Row label={t("code_module.ctx_model")} value={m.model || t("modules_ui.code_ctx_auto")} />
|
|
60
|
+
{session?.mode && <Row label={t("modules_ui.code_ctx_mode")} value={session.mode} />}
|
|
61
|
+
{session?.agentSlug && <Row label={t("modules_ui.code_ctx_agent")} value={session.agentSlug} />}
|
|
62
62
|
<Row
|
|
63
63
|
label={t("code_module.ctx_messages")}
|
|
64
|
-
value={
|
|
64
|
+
value={t("modules_ui.code_ctx_msgs_value", { user: m.userMsgs, assistant: m.assistantMsgs })}
|
|
65
65
|
/>
|
|
66
66
|
<Row label={t("code_module.ctx_input")} value={m.input.toLocaleString()} />
|
|
67
67
|
<Row label={t("code_module.ctx_output")} value={m.output.toLocaleString()} />
|
|
68
|
-
<Row label="
|
|
69
|
-
{session?.createdAt && <Row label="
|
|
70
|
-
{session?.updatedAt && <Row label="
|
|
68
|
+
<Row label={t("modules_ui.code_ctx_tokens_total")} value={(m.input + m.output).toLocaleString()} />
|
|
69
|
+
{session?.createdAt && <Row label={t("modules_ui.code_ctx_created")} value={fmtDate(session.createdAt)} />}
|
|
70
|
+
{session?.updatedAt && <Row label={t("modules_ui.code_ctx_activity")} value={fmtDate(session.updatedAt)} />}
|
|
71
71
|
|
|
72
72
|
<hr className="border-border my-2" />
|
|
73
73
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { FolderGit2 } from "lucide-react";
|
|
2
2
|
import { UiSelect } from "../UiSelect";
|
|
3
|
+
import { t } from "../../i18n";
|
|
3
4
|
import type { ProjectEntry } from "../../types/daemon";
|
|
4
5
|
|
|
5
6
|
interface Props {
|
|
@@ -16,7 +17,7 @@ interface Props {
|
|
|
16
17
|
// their path) so the dropdown is human-readable.
|
|
17
18
|
export function CodeProjectPicker({ projects, value, onChange, disabled }: Props) {
|
|
18
19
|
const options = projects.map((p) => {
|
|
19
|
-
const base = p.path?.split("/").filter(Boolean).pop() ||
|
|
20
|
+
const base = p.path?.split("/").filter(Boolean).pop() || t("modules_ui.code_project_fallback", { id: p.id });
|
|
20
21
|
return {
|
|
21
22
|
value: String(p.id),
|
|
22
23
|
label: p.name || base,
|
|
@@ -31,7 +32,7 @@ export function CodeProjectPicker({ projects, value, onChange, disabled }: Props
|
|
|
31
32
|
value={value}
|
|
32
33
|
onChange={onChange}
|
|
33
34
|
options={options}
|
|
34
|
-
placeholder="
|
|
35
|
+
placeholder={t("modules_ui.code_pick_project_ph")}
|
|
35
36
|
disabled={disabled}
|
|
36
37
|
/>
|
|
37
38
|
</div>
|
|
@@ -5,6 +5,7 @@ import { useState, useEffect, useCallback, Fragment, type ElementType } from "re
|
|
|
5
5
|
import { PanelLeft } from "lucide-react";
|
|
6
6
|
import { cn } from "../../lib/cn";
|
|
7
7
|
import { Tip } from "../ui/tip";
|
|
8
|
+
import { t } from "../../i18n";
|
|
8
9
|
|
|
9
10
|
export interface TabItem {
|
|
10
11
|
key: string;
|
|
@@ -46,11 +47,11 @@ export function useNavCollapse(storageKey: string) {
|
|
|
46
47
|
|
|
47
48
|
export function NavToggle({ collapsed, onToggle }: { collapsed: boolean; onToggle: () => void }) {
|
|
48
49
|
return (
|
|
49
|
-
<Tip content={collapsed ? "
|
|
50
|
+
<Tip content={collapsed ? t("settings_ui.expand_menu") : t("settings_ui.collapse_menu")} side="bottom">
|
|
50
51
|
<button
|
|
51
52
|
type="button"
|
|
52
53
|
onClick={onToggle}
|
|
53
|
-
aria-label={collapsed ? "
|
|
54
|
+
aria-label={collapsed ? t("settings_ui.expand_menu") : t("settings_ui.collapse_menu")}
|
|
54
55
|
className="flex size-7 shrink-0 items-center justify-center rounded-md text-muted-fg transition-colors hover:bg-accent hover:text-foreground"
|
|
55
56
|
>
|
|
56
57
|
<PanelLeft className={cn("size-4 transition-transform", collapsed && "rotate-180")} />
|
|
@@ -4,6 +4,7 @@ import { UiSelect } from "../UiSelect";
|
|
|
4
4
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
|
|
5
5
|
import { getDotted, parseConfigJson } from "../../lib/config-values";
|
|
6
6
|
import { isSecretMarker, secretHint } from "../../lib/secrets";
|
|
7
|
+
import { t } from "../../i18n";
|
|
7
8
|
|
|
8
9
|
export type ConfigField = {
|
|
9
10
|
path: string;
|
|
@@ -27,7 +28,7 @@ export function ConfigTabsEditor({
|
|
|
27
28
|
placeholderSource,
|
|
28
29
|
jsonTitle,
|
|
29
30
|
jsonDescription,
|
|
30
|
-
saveLabel = "
|
|
31
|
+
saveLabel = t("common.save"),
|
|
31
32
|
onSaveFields,
|
|
32
33
|
onSaveJson,
|
|
33
34
|
busy,
|
|
@@ -133,7 +134,7 @@ export function ConfigTabsEditor({
|
|
|
133
134
|
onChange={(event) => setRaw(event.target.value)}
|
|
134
135
|
/>
|
|
135
136
|
{jsonError && <p className="text-xs text-destructive">{jsonError}</p>}
|
|
136
|
-
<Button variant="primary" loading={busy} onClick={saveJson}>
|
|
137
|
+
<Button variant="primary" loading={busy} onClick={saveJson}>{t("settings_ui.save_json")}</Button>
|
|
137
138
|
</div>
|
|
138
139
|
</TabsContent>
|
|
139
140
|
</Tabs>
|
|
@@ -25,13 +25,13 @@ export function GlobalConfigEditor() {
|
|
|
25
25
|
return (
|
|
26
26
|
<Section
|
|
27
27
|
title={t("global_config.title")}
|
|
28
|
-
description="
|
|
28
|
+
description={t("settings_ui.global_config_desc")}
|
|
29
29
|
>
|
|
30
30
|
<ConfigTabsEditor
|
|
31
31
|
sections={GLOBAL_CONFIG_SECTIONS}
|
|
32
32
|
source={config as Record<string, unknown>}
|
|
33
33
|
jsonTitle="~/.apx/config.json"
|
|
34
|
-
jsonDescription="
|
|
34
|
+
jsonDescription={t("settings_ui.global_json_desc")}
|
|
35
35
|
onSaveFields={async (set, unset) => {
|
|
36
36
|
await patch(set, unset);
|
|
37
37
|
mutate();
|