@agentprojectcontext/apx 1.39.1 → 1.40.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/package.json +1 -1
- package/src/core/agent/constants.js +7 -1
- package/src/core/agent/retry.js +9 -0
- package/src/core/agent/run-agent.js +56 -5
- package/src/core/agent/tools/pseudo-tools.js +13 -1
- package/src/core/channels/telegram/dispatch.js +23 -3
- package/src/core/engines/mock.js +33 -10
- package/src/core/i18n/en.js +2 -4
- package/src/core/i18n/es.js +1 -4
- package/src/core/i18n/index.js +5 -1
- package/src/core/i18n/pt.js +1 -3
- package/src/core/routines/runner.js +15 -3
- package/src/host/daemon/api/admin.js +29 -0
- package/src/interfaces/cli/commands/desktop.js +26 -0
- package/src/interfaces/cli/index.js +16 -3
- package/src/interfaces/desktop/main.js +7 -1
- package/src/interfaces/web/dist/assets/index-DW7j3cXB.js +646 -0
- package/src/interfaces/web/dist/assets/index-DW7j3cXB.js.map +1 -0
- package/src/interfaces/web/dist/assets/index-wrEbTJbc.css +1 -0
- package/src/interfaces/web/dist/index.html +2 -2
- package/src/interfaces/web/package-lock.json +188 -188
- package/src/interfaces/web/src/App.tsx +22 -11
- package/src/interfaces/web/src/components/AddProjectDialog.tsx +66 -34
- package/src/interfaces/web/src/components/ModelCombobox.tsx +6 -3
- package/src/interfaces/web/src/components/chat/MessageBubble.tsx +28 -25
- package/src/interfaces/web/src/components/chat/ModelPicker.tsx +19 -17
- package/src/interfaces/web/src/components/deck/WidgetRow.tsx +9 -7
- package/src/interfaces/web/src/components/inputs/VarTokenInput.tsx +21 -19
- package/src/interfaces/web/src/components/layout/ProjectSidebar.tsx +3 -2
- package/src/interfaces/web/src/components/routines/AvailableVarsCard.tsx +23 -0
- package/src/interfaces/web/src/components/routines/ExecutionsList.tsx +189 -0
- package/src/interfaces/web/src/components/routines/ReadOnlyBlock.tsx +14 -0
- package/src/interfaces/web/src/components/routines/RoutineDetail.tsx +86 -0
- package/src/interfaces/web/src/components/routines/RoutineEditor.tsx +263 -0
- package/src/interfaces/web/src/components/routines/RoutineList.tsx +59 -0
- package/src/interfaces/web/src/components/routines/VarTextarea.tsx +70 -0
- package/src/interfaces/web/src/components/routines/shared.ts +89 -0
- package/src/interfaces/web/src/components/settings/PairDeviceDialog.tsx +19 -16
- package/src/interfaces/web/src/components/settings/TelegramContactsPanel.tsx +10 -8
- package/src/interfaces/web/src/components/settings/providers/ProviderModal.tsx +7 -4
- package/src/interfaces/web/src/components/ui/chat-input.tsx +24 -21
- package/src/interfaces/web/src/components/ui/sidebar.tsx +20 -18
- package/src/interfaces/web/src/components/ui.tsx +4 -0
- package/src/interfaces/web/src/i18n/en.ts +34 -11
- package/src/interfaces/web/src/i18n/es.ts +34 -11
- package/src/interfaces/web/src/lib/api/filesystem.ts +6 -0
- package/src/interfaces/web/src/screens/ApxAdminScreen.tsx +11 -3
- package/src/interfaces/web/src/screens/modules/DeckScreen.tsx +6 -3
- package/src/interfaces/web/src/screens/project/AgentDetailScreen.tsx +8 -5
- package/src/interfaces/web/src/screens/project/McpsTab.tsx +16 -9
- package/src/interfaces/web/src/screens/project/RoutinesTab.tsx +126 -373
- package/src/interfaces/web/src/styles.css +5 -0
- package/src/interfaces/web/dist/assets/index-CAKEYko0.css +0 -1
- package/src/interfaces/web/dist/assets/index-UzqHxD0B.js +0 -639
- package/src/interfaces/web/dist/assets/index-UzqHxD0B.js.map +0 -1
|
@@ -15,6 +15,7 @@ import { RobyBubble } from "./components/RobyBubble";
|
|
|
15
15
|
import { Roby, RobyEmpty, type RobyMood } from "./components/Roby";
|
|
16
16
|
import { ToastProvider } from "./components/Toast";
|
|
17
17
|
import { Button } from "./components/ui/button";
|
|
18
|
+
import { Tip } from "./components/ui/tip";
|
|
18
19
|
import { TooltipProvider } from "./components/ui/tooltip";
|
|
19
20
|
import { useTheme } from "./hooks/useTheme";
|
|
20
21
|
import { useProjects } from "./hooks/useProjects";
|
|
@@ -51,7 +52,7 @@ export function App() {
|
|
|
51
52
|
|
|
52
53
|
return (
|
|
53
54
|
<ToastProvider>
|
|
54
|
-
<TooltipProvider delay={
|
|
55
|
+
<TooltipProvider delay={0}>
|
|
55
56
|
<Shell />
|
|
56
57
|
</TooltipProvider>
|
|
57
58
|
</ToastProvider>
|
|
@@ -71,11 +72,20 @@ function Shell() {
|
|
|
71
72
|
next.delete("action");
|
|
72
73
|
setParams(next, { replace: true });
|
|
73
74
|
};
|
|
75
|
+
const openAdd = () => {
|
|
76
|
+
const next = new URLSearchParams(params);
|
|
77
|
+
next.set("action", "add-project");
|
|
78
|
+
setParams(next);
|
|
79
|
+
};
|
|
74
80
|
|
|
75
81
|
return (
|
|
76
82
|
<NavCollapseProvider>
|
|
77
83
|
<div className="flex h-screen w-screen overflow-hidden bg-background text-foreground" data-testid="app-shell">
|
|
78
|
-
<ProjectSidebar
|
|
84
|
+
<ProjectSidebar
|
|
85
|
+
onSelect={(href) => navigate(href)}
|
|
86
|
+
onOpenRoby={() => setRobyOpen(true)}
|
|
87
|
+
onOpenAddProject={openAdd}
|
|
88
|
+
/>
|
|
79
89
|
<main className="m-2 ml-0 flex min-w-0 flex-1 flex-col overflow-hidden rounded-xl border border-border bg-card shadow-sm">
|
|
80
90
|
<TopBar onToggleTheme={toggle} isDark={theme === "dark"} pathname={location.pathname} />
|
|
81
91
|
<div className="flex-1 overflow-y-auto">
|
|
@@ -151,15 +161,16 @@ function TopBar({
|
|
|
151
161
|
</span>
|
|
152
162
|
{pageActions}
|
|
153
163
|
<LanguageMenu />
|
|
154
|
-
<
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
164
|
+
<Tip content={isDark ? t("topbar.light") : t("topbar.dark")}>
|
|
165
|
+
<button
|
|
166
|
+
type="button"
|
|
167
|
+
data-testid="theme-toggle"
|
|
168
|
+
onClick={onToggleTheme}
|
|
169
|
+
className="shrink-0 rounded-md p-1.5 text-muted-fg hover:bg-accent hover:text-accent-fg"
|
|
170
|
+
>
|
|
171
|
+
{isDark ? <Sun size={14} /> : <Moon size={14} />}
|
|
172
|
+
</button>
|
|
173
|
+
</Tip>
|
|
163
174
|
</header>
|
|
164
175
|
);
|
|
165
176
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
|
+
import { useNavigate } from "react-router-dom";
|
|
2
3
|
import { useSWRConfig } from "swr";
|
|
3
|
-
import { FolderOpen, Home, Search } from "lucide-react";
|
|
4
|
+
import { FolderOpen, Home, Search, X } from "lucide-react";
|
|
4
5
|
import { Filesystem, Projects } from "../lib/api";
|
|
5
6
|
import { Button, Dialog, Empty, Field, Input, Loading } from "./ui";
|
|
6
7
|
import { useToast } from "./Toast";
|
|
@@ -8,8 +9,10 @@ import { t } from "../i18n";
|
|
|
8
9
|
|
|
9
10
|
export function AddProjectDialog({ open, onClose }: { open: boolean; onClose: () => void }) {
|
|
10
11
|
const { mutate } = useSWRConfig();
|
|
12
|
+
const navigate = useNavigate();
|
|
11
13
|
const toast = useToast();
|
|
12
14
|
const [path, setPath] = useState("");
|
|
15
|
+
const [browseOpen, setBrowseOpen] = useState(false);
|
|
13
16
|
const [browsePath, setBrowsePath] = useState("");
|
|
14
17
|
const [entries, setEntries] = useState<string[]>([]);
|
|
15
18
|
const [parent, setParent] = useState<string | null>(null);
|
|
@@ -35,10 +38,34 @@ export function AddProjectDialog({ open, onClose }: { open: boolean; onClose: ()
|
|
|
35
38
|
}
|
|
36
39
|
};
|
|
37
40
|
|
|
41
|
+
// Reset everything when the dialog closes so reopening starts fresh.
|
|
38
42
|
useEffect(() => {
|
|
39
|
-
if (open
|
|
43
|
+
if (open) return;
|
|
44
|
+
setPath("");
|
|
45
|
+
setBrowseOpen(false);
|
|
46
|
+
setBrowsePath("");
|
|
47
|
+
setEntries([]);
|
|
48
|
+
setParent(null);
|
|
49
|
+
setBrowseError("");
|
|
40
50
|
}, [open]);
|
|
41
51
|
|
|
52
|
+
const openBrowser = async () => {
|
|
53
|
+
// Try the OS-native folder picker first (osascript / zenity / PowerShell).
|
|
54
|
+
// If the daemon can't open one, fall back to the inline directory list.
|
|
55
|
+
setLoadingDirs(true);
|
|
56
|
+
try {
|
|
57
|
+
const out = await Filesystem.pickDir(t("add_project.picker_prompt"));
|
|
58
|
+
if ("cancelled" in out) return;
|
|
59
|
+
setPath(out.path);
|
|
60
|
+
return;
|
|
61
|
+
} catch {
|
|
62
|
+
setBrowseOpen(true);
|
|
63
|
+
await loadDirs(path || "~");
|
|
64
|
+
} finally {
|
|
65
|
+
setLoadingDirs(false);
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
42
69
|
const submit = async () => {
|
|
43
70
|
const trimmed = path.trim();
|
|
44
71
|
if (!trimmed) { toast.error(t("add_project.path_required")); return; }
|
|
@@ -47,8 +74,8 @@ export function AddProjectDialog({ open, onClose }: { open: boolean; onClose: ()
|
|
|
47
74
|
const out = await Projects.register(trimmed);
|
|
48
75
|
toast.success(t("add_project.registered", { id: out.id }));
|
|
49
76
|
await mutate("/projects");
|
|
50
|
-
setPath("");
|
|
51
77
|
onClose();
|
|
78
|
+
navigate(`/p/${out.id}`);
|
|
52
79
|
} catch (e) {
|
|
53
80
|
toast.error((e as Error).message);
|
|
54
81
|
} finally { setBusy(false); }
|
|
@@ -77,44 +104,49 @@ export function AddProjectDialog({ open, onClose }: { open: boolean; onClose: ()
|
|
|
77
104
|
onChange={(e) => setPath(e.target.value)}
|
|
78
105
|
onKeyDown={(e) => { if (e.key === "Enter") submit(); }}
|
|
79
106
|
/>
|
|
80
|
-
<Button onClick={
|
|
107
|
+
<Button onClick={openBrowser} disabled={loadingDirs}>
|
|
81
108
|
<Search size={14} /> {t("add_project.search_btn")}
|
|
82
109
|
</Button>
|
|
83
110
|
</div>
|
|
84
111
|
</Field>
|
|
85
112
|
|
|
86
|
-
|
|
87
|
-
<div className="
|
|
88
|
-
<
|
|
89
|
-
|
|
90
|
-
<
|
|
91
|
-
<
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
113
|
+
{browseOpen && (
|
|
114
|
+
<div className="rounded-md border border-border bg-muted/20">
|
|
115
|
+
<div className="flex items-center justify-between border-b border-border px-3 py-2">
|
|
116
|
+
<span className="truncate font-mono text-xs text-muted-fg">{browsePath || path || "~"}</span>
|
|
117
|
+
<div className="flex gap-1">
|
|
118
|
+
<Button size="sm" variant="ghost" onClick={() => loadDirs("~")} disabled={loadingDirs}>
|
|
119
|
+
<Home size={13} />
|
|
120
|
+
</Button>
|
|
121
|
+
<Button size="sm" variant="ghost" onClick={() => parent && loadDirs(parent)} disabled={!parent || loadingDirs}>
|
|
122
|
+
..
|
|
123
|
+
</Button>
|
|
124
|
+
<Button size="sm" variant="ghost" onClick={() => setBrowseOpen(false)} disabled={loadingDirs}>
|
|
125
|
+
<X size={13} />
|
|
126
|
+
</Button>
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
<div className="max-h-64 overflow-y-auto p-2">
|
|
130
|
+
{loadingDirs && <Loading />}
|
|
131
|
+
{!loadingDirs && browseError && (
|
|
132
|
+
<Empty>{t("add_project.browser_unavailable")}</Empty>
|
|
133
|
+
)}
|
|
134
|
+
{!loadingDirs && !browseError && entries.length === 0 && <Empty>{t("add_project.no_folders")}</Empty>}
|
|
135
|
+
{!loadingDirs && !browseError && entries.map((entry) => (
|
|
136
|
+
<button
|
|
137
|
+
key={entry}
|
|
138
|
+
type="button"
|
|
139
|
+
onClick={() => loadDirs(entry)}
|
|
140
|
+
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm hover:bg-accent"
|
|
141
|
+
>
|
|
142
|
+
<FolderOpen size={14} className="text-muted-fg" />
|
|
143
|
+
<span className="truncate">{entry.split("/").pop()}</span>
|
|
144
|
+
<span className="ml-auto truncate font-mono text-[10px] text-muted-fg">{entry}</span>
|
|
145
|
+
</button>
|
|
146
|
+
))}
|
|
96
147
|
</div>
|
|
97
148
|
</div>
|
|
98
|
-
|
|
99
|
-
{loadingDirs && <Loading />}
|
|
100
|
-
{!loadingDirs && browseError && (
|
|
101
|
-
<Empty>{t("add_project.browser_unavailable")}</Empty>
|
|
102
|
-
)}
|
|
103
|
-
{!loadingDirs && !browseError && entries.length === 0 && <Empty>{t("add_project.no_folders")}</Empty>}
|
|
104
|
-
{!loadingDirs && !browseError && entries.map((entry) => (
|
|
105
|
-
<button
|
|
106
|
-
key={entry}
|
|
107
|
-
type="button"
|
|
108
|
-
onClick={() => loadDirs(entry)}
|
|
109
|
-
className="flex w-full items-center gap-2 rounded-md px-2 py-1.5 text-left text-sm hover:bg-accent"
|
|
110
|
-
>
|
|
111
|
-
<FolderOpen size={14} className="text-muted-fg" />
|
|
112
|
-
<span className="truncate">{entry.split("/").pop()}</span>
|
|
113
|
-
<span className="ml-auto truncate font-mono text-[10px] text-muted-fg">{entry}</span>
|
|
114
|
-
</button>
|
|
115
|
-
))}
|
|
116
|
-
</div>
|
|
117
|
-
</div>
|
|
149
|
+
)}
|
|
118
150
|
</div>
|
|
119
151
|
</Dialog>
|
|
120
152
|
);
|
|
@@ -2,6 +2,7 @@ import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
3
|
import { AlertTriangle, ChevronDown } from "lucide-react";
|
|
4
4
|
import { cn } from "../lib/cn";
|
|
5
|
+
import { Tip } from "./ui/tip";
|
|
5
6
|
import { t } from "../i18n";
|
|
6
7
|
|
|
7
8
|
// Editable combobox: type freely, matching options appear below; click one to
|
|
@@ -83,9 +84,11 @@ export function ModelCombobox({
|
|
|
83
84
|
)}
|
|
84
85
|
>
|
|
85
86
|
{invalid && (
|
|
86
|
-
<
|
|
87
|
-
<
|
|
88
|
-
|
|
87
|
+
<Tip content={invalidHint || t("models_ui.invalid_hint")}>
|
|
88
|
+
<span>
|
|
89
|
+
<AlertTriangle className="size-3.5 shrink-0 text-amber-400" />
|
|
90
|
+
</span>
|
|
91
|
+
</Tip>
|
|
89
92
|
)}
|
|
90
93
|
<input
|
|
91
94
|
value={query}
|
|
@@ -4,6 +4,7 @@ import { ToolCall } from "./ToolCall";
|
|
|
4
4
|
import { AskQuestionsCard } from "./AskQuestionsCard";
|
|
5
5
|
import { AskAnswersCard, parseAskAnswerText } from "./AskAnswersCard";
|
|
6
6
|
import { textOf, type ChatMsg } from "../../hooks/useChat";
|
|
7
|
+
import { Tip } from "../ui/tip";
|
|
7
8
|
import { t } from "../../i18n";
|
|
8
9
|
|
|
9
10
|
interface Props {
|
|
@@ -51,22 +52,23 @@ export function MessageBubble({ msg, isLast, isAskAnswer, onCopy }: Props) {
|
|
|
51
52
|
|
|
52
53
|
{/* Skill Inspector: which skills the per-turn RAG injected for this turn. */}
|
|
53
54
|
{!mine && msg.inspector && (msg.inspector.loaded?.length || msg.inspector.hinted?.length) ? (
|
|
54
|
-
<
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
55
|
+
<Tip content={t("shared_ui.skill_inspector_title", { embedder: msg.inspector.embedder || "RAG" })}>
|
|
56
|
+
<div
|
|
57
|
+
className="flex flex-wrap items-center gap-1 text-[10px] text-sky-400/90"
|
|
58
|
+
>
|
|
59
|
+
<Sparkles size={10} />
|
|
60
|
+
{msg.inspector.loaded?.map((s) => (
|
|
61
|
+
<span key={`l-${s}`} className="rounded bg-sky-500/15 px-1 py-0.5 font-mono">
|
|
62
|
+
{s}
|
|
63
|
+
</span>
|
|
64
|
+
))}
|
|
65
|
+
{msg.inspector.hinted?.map((s) => (
|
|
66
|
+
<span key={`h-${s}`} className="rounded border border-sky-500/30 px-1 py-0.5 font-mono opacity-70">
|
|
67
|
+
{s}?
|
|
68
|
+
</span>
|
|
69
|
+
))}
|
|
70
|
+
</div>
|
|
71
|
+
</Tip>
|
|
70
72
|
) : null}
|
|
71
73
|
|
|
72
74
|
{/* Ordered parts: interleaved assistant text + tool calls. */}
|
|
@@ -110,15 +112,16 @@ export function MessageBubble({ msg, isLast, isAskAnswer, onCopy }: Props) {
|
|
|
110
112
|
<span>· {t("shared_ui.tools_count", { n: msg.parts.filter((p) => p.kind === "tool").length })}</span>
|
|
111
113
|
)}
|
|
112
114
|
{onCopy && copyText && (
|
|
113
|
-
<
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
115
|
+
<Tip content={t("chat_ui.copy")}>
|
|
116
|
+
<button
|
|
117
|
+
type="button"
|
|
118
|
+
onClick={() => onCopy(copyText)}
|
|
119
|
+
className="inline-flex items-center gap-1 hover:text-foreground"
|
|
120
|
+
aria-label={t("chat_ui.copy")}
|
|
121
|
+
>
|
|
122
|
+
<Copy size={10} /> {t("chat_ui.copy")}
|
|
123
|
+
</button>
|
|
124
|
+
</Tip>
|
|
122
125
|
)}
|
|
123
126
|
</div>
|
|
124
127
|
</div>
|
|
@@ -2,6 +2,7 @@ import { useEffect, useRef, useState } from "react";
|
|
|
2
2
|
import { Server, ChevronDown, X, Check } from "lucide-react";
|
|
3
3
|
import { cn } from "../../lib/cn";
|
|
4
4
|
import { Engines } from "../../lib/api";
|
|
5
|
+
import { Tip } from "../ui/tip";
|
|
5
6
|
import { t } from "../../i18n";
|
|
6
7
|
|
|
7
8
|
// Compact model picker for the chat composer (the panda.project pattern): a
|
|
@@ -70,23 +71,24 @@ export function ModelPicker({
|
|
|
70
71
|
|
|
71
72
|
return (
|
|
72
73
|
<div ref={wrapRef} className="relative">
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
74
|
+
<Tip content={t("chat_ui.pick_model")}>
|
|
75
|
+
<button
|
|
76
|
+
type="button"
|
|
77
|
+
disabled={disabled}
|
|
78
|
+
onClick={() => setOpen((v) => !v)}
|
|
79
|
+
data-testid="chat-model-picker"
|
|
80
|
+
className={cn(
|
|
81
|
+
"flex max-w-[200px] items-center gap-1 rounded-md border border-transparent px-1.5 py-0.5 text-[11px] text-muted-foreground transition-colors",
|
|
82
|
+
"hover:bg-accent/60 hover:text-foreground",
|
|
83
|
+
value && "text-foreground",
|
|
84
|
+
)}
|
|
85
|
+
aria-label={t("chat_ui.pick_model")}
|
|
86
|
+
>
|
|
87
|
+
<Server className="size-3 shrink-0" />
|
|
88
|
+
<span className="truncate font-mono">{label}</span>
|
|
89
|
+
<ChevronDown className="size-3 shrink-0 opacity-60" />
|
|
90
|
+
</button>
|
|
91
|
+
</Tip>
|
|
90
92
|
|
|
91
93
|
{open && (
|
|
92
94
|
<div className="absolute bottom-full left-0 z-50 mb-1.5 w-64 rounded-lg border border-border bg-popover p-1.5 shadow-md ring-1 ring-foreground/10">
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { useState } from "react";
|
|
2
2
|
import { Badge, Switch } from "../ui";
|
|
3
|
+
import { Tip } from "../ui/tip";
|
|
3
4
|
import { cn } from "../../lib/cn";
|
|
4
5
|
import type { DeckWidget } from "../../lib/api/deck";
|
|
5
6
|
import { t } from "../../i18n";
|
|
@@ -63,13 +64,14 @@ export function WidgetRow({ widget, onToggle }: WidgetRowProps) {
|
|
|
63
64
|
)}
|
|
64
65
|
>
|
|
65
66
|
{/* Source dot */}
|
|
66
|
-
<
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
<Tip content={widget.source === "apx" ? t("deck_screen.widget_native") : t("deck_screen.widget_external")}>
|
|
68
|
+
<span
|
|
69
|
+
className={cn(
|
|
70
|
+
"size-2 shrink-0 rounded-full",
|
|
71
|
+
widget.source === "apx" ? "bg-emerald-500" : "bg-sky-400"
|
|
72
|
+
)}
|
|
73
|
+
/>
|
|
74
|
+
</Tip>
|
|
73
75
|
|
|
74
76
|
{/* Title + desktop */}
|
|
75
77
|
<div className="min-w-0 flex-1">
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from "react";
|
|
9
9
|
import { Plus } from "lucide-react";
|
|
10
10
|
import { cn } from "../../lib/cn";
|
|
11
|
+
import { Tip } from "../ui/tip";
|
|
11
12
|
import { t } from "../../i18n";
|
|
12
13
|
|
|
13
14
|
// VarTokenInput
|
|
@@ -285,25 +286,26 @@ export const VarTokenInput = forwardRef<VarTokenInputHandle, VarTokenInputProps>
|
|
|
285
286
|
)}
|
|
286
287
|
</div>
|
|
287
288
|
<div className="relative flex">
|
|
288
|
-
<
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
e
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
289
|
+
<Tip content={t("chat_ui.insert_variable")}>
|
|
290
|
+
<button
|
|
291
|
+
type="button"
|
|
292
|
+
// onMouseDown prevents the editor from blurring before we capture
|
|
293
|
+
// the saved range — that's what kept the caret jumping to start.
|
|
294
|
+
onMouseDown={(e) => {
|
|
295
|
+
e.preventDefault();
|
|
296
|
+
saveSelection();
|
|
297
|
+
}}
|
|
298
|
+
onClick={() => setPickerOpen((v) => !v)}
|
|
299
|
+
aria-label={t("chat_ui.insert_variable")}
|
|
300
|
+
className={cn(
|
|
301
|
+
"flex items-center justify-center px-2 min-w-8 border-l border-input text-muted-foreground rounded-r-lg",
|
|
302
|
+
"hover:bg-muted/60 hover:text-foreground transition-colors",
|
|
303
|
+
pickerOpen && "bg-muted/60 text-foreground",
|
|
304
|
+
)}
|
|
305
|
+
>
|
|
306
|
+
<Plus size={14} />
|
|
307
|
+
</button>
|
|
308
|
+
</Tip>
|
|
307
309
|
{pickerOpen && (
|
|
308
310
|
<VarPickerPopover
|
|
309
311
|
query={pickerQuery}
|
|
@@ -31,6 +31,7 @@ import type { ProjectEntry } from "../../types/daemon";
|
|
|
31
31
|
interface Props {
|
|
32
32
|
onSelect: (href: string) => void;
|
|
33
33
|
onOpenRoby: () => void;
|
|
34
|
+
onOpenAddProject?: () => void;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
37
|
interface ModuleItem {
|
|
@@ -158,7 +159,7 @@ function RailProjectMenu({
|
|
|
158
159
|
);
|
|
159
160
|
}
|
|
160
161
|
|
|
161
|
-
export function ProjectSidebar({ onSelect, onOpenRoby }: Props) {
|
|
162
|
+
export function ProjectSidebar({ onSelect, onOpenRoby, onOpenAddProject }: Props) {
|
|
162
163
|
const { projects, isLoading } = useProjects();
|
|
163
164
|
const location = useLocation();
|
|
164
165
|
const MODULES = buildModules();
|
|
@@ -303,7 +304,7 @@ export function ProjectSidebar({ onSelect, onOpenRoby }: Props) {
|
|
|
303
304
|
testId="nav-add-project"
|
|
304
305
|
icon={<Plus size={18} />}
|
|
305
306
|
active={false}
|
|
306
|
-
onClick={() => onSelect("/?action=add-project")}
|
|
307
|
+
onClick={() => (onOpenAddProject ? onOpenAddProject() : onSelect("/?action=add-project"))}
|
|
307
308
|
title={t("nav.add_project")}
|
|
308
309
|
/>
|
|
309
310
|
</div>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Tip } from "../ui";
|
|
2
|
+
import { t } from "../../i18n";
|
|
3
|
+
import { routineVars } from "./shared";
|
|
4
|
+
|
|
5
|
+
// Reference card (lives under the options column): every variable with its
|
|
6
|
+
// context tag + a hover tooltip explaining it. The click-to-insert chips under
|
|
7
|
+
// each textarea are tooltip-free — the explanation lives here.
|
|
8
|
+
export function AvailableVarsCard() {
|
|
9
|
+
return (
|
|
10
|
+
<div className="rounded-lg border border-border bg-muted/10 p-3">
|
|
11
|
+
<div className="mb-1.5 text-[11px] font-semibold uppercase tracking-wide text-muted-fg">{t("project.routines.vars_title")}</div>
|
|
12
|
+
<div className="flex flex-wrap gap-1.5">
|
|
13
|
+
{routineVars().map((v) => (
|
|
14
|
+
<Tip key={v.v} content={<span className="block max-w-[240px] whitespace-normal leading-snug">{v.desc}</span>}>
|
|
15
|
+
<span className="inline-flex cursor-help items-center gap-1 rounded-md border border-border bg-card px-1.5 py-0.5 font-mono text-[10px]">
|
|
16
|
+
{v.v}<span className="not-italic text-muted-fg">· {v.where}</span>
|
|
17
|
+
</span>
|
|
18
|
+
</Tip>
|
|
19
|
+
))}
|
|
20
|
+
</div>
|
|
21
|
+
</div>
|
|
22
|
+
);
|
|
23
|
+
}
|