@cryptiklemur/lattice 0.0.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.
- package/.editorconfig +12 -0
- package/.github/workflows/release.yml +44 -0
- package/.impeccable.md +66 -0
- package/.releaserc.json +32 -0
- package/.serena/project.yml +138 -0
- package/CLAUDE.md +35 -0
- package/CONTRIBUTING.md +93 -0
- package/LICENSE +21 -0
- package/README.md +83 -0
- package/bun.lock +1459 -0
- package/bunfig.toml +2 -0
- package/client/index.html +32 -0
- package/client/package.json +37 -0
- package/client/public/icons/icon-192.svg +11 -0
- package/client/public/icons/icon-512.svg +11 -0
- package/client/public/manifest.json +24 -0
- package/client/public/sw.js +61 -0
- package/client/src/App.tsx +28 -0
- package/client/src/components/auth/PassphrasePrompt.tsx +70 -0
- package/client/src/components/chat/ChatInput.tsx +241 -0
- package/client/src/components/chat/ChatView.tsx +727 -0
- package/client/src/components/chat/Message.tsx +362 -0
- package/client/src/components/chat/ModelSelector.tsx +87 -0
- package/client/src/components/chat/PermissionModeSelector.tsx +41 -0
- package/client/src/components/chat/StatusBar.tsx +50 -0
- package/client/src/components/chat/ToolGroup.tsx +129 -0
- package/client/src/components/chat/ToolResultRenderer.tsx +343 -0
- package/client/src/components/chat/toolSummary.ts +41 -0
- package/client/src/components/dashboard/DashboardView.tsx +219 -0
- package/client/src/components/dashboard/ProjectDashboardView.tsx +168 -0
- package/client/src/components/mesh/NodeBadge.tsx +24 -0
- package/client/src/components/mesh/PairingDialog.tsx +281 -0
- package/client/src/components/panels/FileBrowser.tsx +241 -0
- package/client/src/components/panels/StickyNotes.tsx +187 -0
- package/client/src/components/panels/Terminal.tsx +128 -0
- package/client/src/components/project-settings/ProjectClaude.tsx +304 -0
- package/client/src/components/project-settings/ProjectEnvironment.tsx +235 -0
- package/client/src/components/project-settings/ProjectGeneral.tsx +76 -0
- package/client/src/components/project-settings/ProjectMcp.tsx +232 -0
- package/client/src/components/project-settings/ProjectPermissions.tsx +209 -0
- package/client/src/components/project-settings/ProjectRules.tsx +277 -0
- package/client/src/components/project-settings/ProjectSettingsView.tsx +99 -0
- package/client/src/components/project-settings/ProjectSkills.tsx +91 -0
- package/client/src/components/settings/Appearance.tsx +151 -0
- package/client/src/components/settings/ClaudeSettings.tsx +151 -0
- package/client/src/components/settings/Environment.tsx +185 -0
- package/client/src/components/settings/GlobalMcp.tsx +207 -0
- package/client/src/components/settings/GlobalSkills.tsx +125 -0
- package/client/src/components/settings/MeshStatus.tsx +145 -0
- package/client/src/components/settings/SettingsView.tsx +57 -0
- package/client/src/components/settings/SkillMarketplace.tsx +175 -0
- package/client/src/components/settings/mcp-shared.tsx +194 -0
- package/client/src/components/settings/skill-shared.tsx +177 -0
- package/client/src/components/setup/SetupWizard.tsx +750 -0
- package/client/src/components/sidebar/NodeSettingsModal.tsx +180 -0
- package/client/src/components/sidebar/ProjectDropdown.tsx +43 -0
- package/client/src/components/sidebar/ProjectRail.tsx +291 -0
- package/client/src/components/sidebar/SearchFilter.tsx +52 -0
- package/client/src/components/sidebar/SessionList.tsx +384 -0
- package/client/src/components/sidebar/SettingsSidebar.tsx +128 -0
- package/client/src/components/sidebar/Sidebar.tsx +209 -0
- package/client/src/components/sidebar/UserIsland.tsx +59 -0
- package/client/src/components/sidebar/UserMenu.tsx +101 -0
- package/client/src/components/ui/CommandPalette.tsx +321 -0
- package/client/src/components/ui/ErrorBoundary.tsx +56 -0
- package/client/src/components/ui/IconPicker.tsx +209 -0
- package/client/src/components/ui/LatticeLogomark.tsx +19 -0
- package/client/src/components/ui/PopupMenu.tsx +98 -0
- package/client/src/components/ui/SaveFooter.tsx +38 -0
- package/client/src/components/ui/Toast.tsx +112 -0
- package/client/src/hooks/useMesh.ts +89 -0
- package/client/src/hooks/useProjectSettings.ts +56 -0
- package/client/src/hooks/useProjects.ts +66 -0
- package/client/src/hooks/useSaveState.ts +59 -0
- package/client/src/hooks/useSession.ts +317 -0
- package/client/src/hooks/useSidebar.ts +74 -0
- package/client/src/hooks/useSkills.ts +30 -0
- package/client/src/hooks/useTheme.ts +114 -0
- package/client/src/hooks/useWebSocket.ts +26 -0
- package/client/src/main.tsx +10 -0
- package/client/src/providers/WebSocketProvider.tsx +146 -0
- package/client/src/router.tsx +391 -0
- package/client/src/stores/mesh.ts +78 -0
- package/client/src/stores/session.ts +322 -0
- package/client/src/stores/sidebar.ts +336 -0
- package/client/src/stores/theme.ts +44 -0
- package/client/src/styles/global.css +167 -0
- package/client/src/styles/theme-vars.css +18 -0
- package/client/src/themes/index.ts +79 -0
- package/client/src/utils/findDuplicateKeys.ts +12 -0
- package/client/tsconfig.json +14 -0
- package/client/vite.config.ts +20 -0
- package/package.json +46 -0
- package/server/package.json +22 -0
- package/server/src/auth/passphrase.ts +48 -0
- package/server/src/config.ts +55 -0
- package/server/src/daemon.ts +338 -0
- package/server/src/features/ralph-loop.ts +173 -0
- package/server/src/features/scheduler.ts +281 -0
- package/server/src/features/sticky-notes.ts +102 -0
- package/server/src/handlers/chat.ts +194 -0
- package/server/src/handlers/fs.ts +84 -0
- package/server/src/handlers/loop.ts +37 -0
- package/server/src/handlers/mesh.ts +125 -0
- package/server/src/handlers/notes.ts +45 -0
- package/server/src/handlers/project-settings.ts +174 -0
- package/server/src/handlers/scheduler.ts +47 -0
- package/server/src/handlers/session.ts +159 -0
- package/server/src/handlers/settings.ts +109 -0
- package/server/src/handlers/skills.ts +380 -0
- package/server/src/handlers/terminal.ts +70 -0
- package/server/src/identity.ts +26 -0
- package/server/src/index.ts +190 -0
- package/server/src/mesh/connector.ts +209 -0
- package/server/src/mesh/discovery.ts +123 -0
- package/server/src/mesh/pairing.ts +94 -0
- package/server/src/mesh/peers.ts +52 -0
- package/server/src/mesh/proxy.ts +103 -0
- package/server/src/mesh/session-sync.ts +107 -0
- package/server/src/project/context-breakdown.ts +289 -0
- package/server/src/project/file-browser.ts +106 -0
- package/server/src/project/project-files.ts +267 -0
- package/server/src/project/registry.ts +57 -0
- package/server/src/project/sdk-bridge.ts +566 -0
- package/server/src/project/session.ts +432 -0
- package/server/src/project/terminal.ts +69 -0
- package/server/src/tls.ts +51 -0
- package/server/src/ws/broadcast.ts +31 -0
- package/server/src/ws/router.ts +104 -0
- package/server/src/ws/server.ts +2 -0
- package/server/tsconfig.json +16 -0
- package/shared/package.json +11 -0
- package/shared/src/constants.ts +7 -0
- package/shared/src/index.ts +4 -0
- package/shared/src/messages.ts +638 -0
- package/shared/src/models.ts +136 -0
- package/shared/src/project-settings.ts +45 -0
- package/shared/tsconfig.json +11 -0
- package/themes/amoled.json +20 -0
- package/themes/ayu-light.json +9 -0
- package/themes/catppuccin-latte.json +9 -0
- package/themes/catppuccin-mocha.json +9 -0
- package/themes/clay-light.json +10 -0
- package/themes/clay.json +10 -0
- package/themes/dracula.json +9 -0
- package/themes/everforest-light.json +9 -0
- package/themes/everforest.json +9 -0
- package/themes/github-light.json +9 -0
- package/themes/gruvbox-dark.json +9 -0
- package/themes/gruvbox-light.json +9 -0
- package/themes/monokai.json +9 -0
- package/themes/nord-light.json +9 -0
- package/themes/nord.json +9 -0
- package/themes/one-dark.json +9 -0
- package/themes/one-light.json +9 -0
- package/themes/rose-pine-dawn.json +9 -0
- package/themes/rose-pine.json +9 -0
- package/themes/solarized-dark.json +9 -0
- package/themes/solarized-light.json +9 -0
- package/themes/tokyo-night-light.json +9 -0
- package/themes/tokyo-night.json +9 -0
- package/tsconfig.json +26 -0
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { ChevronDown, ChevronRight } from "lucide-react";
|
|
3
|
+
import { SaveFooter } from "../ui/SaveFooter";
|
|
4
|
+
import { useSaveState } from "../../hooks/useSaveState";
|
|
5
|
+
import type { ProjectSettings, ThinkingConfig } from "@lattice/shared";
|
|
6
|
+
|
|
7
|
+
var CLAUDE_MODELS = [
|
|
8
|
+
{ id: "claude-opus-4-6", label: "Claude Opus 4.6" },
|
|
9
|
+
{ id: "claude-sonnet-4-6", label: "Claude Sonnet 4.6" },
|
|
10
|
+
{ id: "claude-haiku-4-5", label: "Claude Haiku 4.5" },
|
|
11
|
+
{ id: "claude-opus-4-5", label: "Claude Opus 4.5" },
|
|
12
|
+
{ id: "claude-sonnet-4-5", label: "Claude Sonnet 4.5" },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
var EFFORT_LEVELS = [
|
|
16
|
+
{ id: "low", label: "Low" },
|
|
17
|
+
{ id: "normal", label: "Normal" },
|
|
18
|
+
{ id: "high", label: "High" },
|
|
19
|
+
{ id: "max", label: "Max" },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
var THINKING_MODES = [
|
|
23
|
+
{ id: "adaptive", label: "Adaptive" },
|
|
24
|
+
{ id: "enabled", label: "Enabled" },
|
|
25
|
+
{ id: "disabled", label: "Disabled" },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
var PERMISSION_MODES = [
|
|
29
|
+
{ id: "default", label: "Default" },
|
|
30
|
+
{ id: "acceptEdits", label: "Accept Edits" },
|
|
31
|
+
{ id: "plan", label: "Plan" },
|
|
32
|
+
{ id: "dontAsk", label: "Don't Ask" },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
function thinkingLabel(t?: ThinkingConfig): string {
|
|
36
|
+
if (!t) return "Adaptive";
|
|
37
|
+
if (t.type === "adaptive") return "Adaptive";
|
|
38
|
+
if (t.type === "enabled") return "Enabled";
|
|
39
|
+
return "Disabled";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function modelLabel(id: string): string {
|
|
43
|
+
var found = CLAUDE_MODELS.find(function (m) { return m.id === id; });
|
|
44
|
+
return found ? found.label : id;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
interface ProjectClaudeProps {
|
|
48
|
+
settings: ProjectSettings;
|
|
49
|
+
updateSection: (section: string, data: Record<string, unknown>) => void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function ProjectClaude({ settings, updateSection }: ProjectClaudeProps) {
|
|
53
|
+
var [claudeMd, setClaudeMd] = useState(settings.claudeMd ?? "");
|
|
54
|
+
var [defaultModel, setDefaultModel] = useState<string | undefined>(settings.defaultModel);
|
|
55
|
+
var [defaultEffort, setDefaultEffort] = useState<string | undefined>(settings.defaultEffort);
|
|
56
|
+
var [thinking, setThinking] = useState<ThinkingConfig | undefined>(settings.thinking);
|
|
57
|
+
var [permissionMode, setPermissionMode] = useState<string | undefined>(settings.permissionMode);
|
|
58
|
+
var [budgetTokens, setBudgetTokens] = useState<number>(
|
|
59
|
+
settings.thinking?.type === "enabled" ? (settings.thinking.budgetTokens ?? 10000) : 10000,
|
|
60
|
+
);
|
|
61
|
+
var [showGlobalMd, setShowGlobalMd] = useState(false);
|
|
62
|
+
var save = useSaveState();
|
|
63
|
+
|
|
64
|
+
useEffect(function () {
|
|
65
|
+
if (save.saving) {
|
|
66
|
+
save.confirmSave();
|
|
67
|
+
} else {
|
|
68
|
+
setClaudeMd(settings.claudeMd ?? "");
|
|
69
|
+
setDefaultModel(settings.defaultModel);
|
|
70
|
+
setDefaultEffort(settings.defaultEffort);
|
|
71
|
+
setThinking(settings.thinking);
|
|
72
|
+
setPermissionMode(settings.permissionMode);
|
|
73
|
+
if (settings.thinking?.type === "enabled") {
|
|
74
|
+
setBudgetTokens(settings.thinking.budgetTokens ?? 10000);
|
|
75
|
+
}
|
|
76
|
+
save.resetFromServer();
|
|
77
|
+
}
|
|
78
|
+
}, [settings]);
|
|
79
|
+
|
|
80
|
+
function handleSave() {
|
|
81
|
+
save.startSave();
|
|
82
|
+
var thinkingValue: ThinkingConfig | undefined = thinking;
|
|
83
|
+
if (thinkingValue?.type === "enabled") {
|
|
84
|
+
thinkingValue = { type: "enabled", budgetTokens };
|
|
85
|
+
}
|
|
86
|
+
updateSection("claude", {
|
|
87
|
+
claudeMd: claudeMd || undefined,
|
|
88
|
+
defaultModel,
|
|
89
|
+
defaultEffort,
|
|
90
|
+
thinking: thinkingValue,
|
|
91
|
+
permissionMode,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
var globalModel = settings.global.defaultModel || CLAUDE_MODELS[0].id;
|
|
96
|
+
var globalEffort = settings.global.defaultEffort || "normal";
|
|
97
|
+
|
|
98
|
+
return (
|
|
99
|
+
<div className="py-2 space-y-6">
|
|
100
|
+
<div>
|
|
101
|
+
<label htmlFor="project-claude-md" className="block text-[12px] font-semibold text-base-content/40 mb-2">
|
|
102
|
+
Project CLAUDE.md
|
|
103
|
+
</label>
|
|
104
|
+
<textarea
|
|
105
|
+
id="project-claude-md"
|
|
106
|
+
value={claudeMd}
|
|
107
|
+
onChange={function (e) { setClaudeMd(e.target.value); save.markDirty(); }}
|
|
108
|
+
placeholder="# Project-specific instructions for Claude..."
|
|
109
|
+
rows={10}
|
|
110
|
+
className="w-full px-3 py-2.5 bg-base-300 border border-base-content/15 rounded-xl text-base-content text-[12px] font-mono leading-relaxed resize-y focus:border-primary focus-visible:outline-none transition-colors duration-[120ms]"
|
|
111
|
+
/>
|
|
112
|
+
<button
|
|
113
|
+
type="button"
|
|
114
|
+
onClick={function () { setShowGlobalMd(!showGlobalMd); }}
|
|
115
|
+
className="flex items-center gap-1.5 mt-2 text-[11px] text-base-content/40 hover:text-base-content/60 transition-colors"
|
|
116
|
+
>
|
|
117
|
+
{showGlobalMd
|
|
118
|
+
? <ChevronDown size={12} />
|
|
119
|
+
: <ChevronRight size={12} />}
|
|
120
|
+
Global CLAUDE.md (inherited)
|
|
121
|
+
</button>
|
|
122
|
+
{showGlobalMd && (
|
|
123
|
+
<textarea
|
|
124
|
+
readOnly
|
|
125
|
+
value={settings.global.claudeMd}
|
|
126
|
+
rows={8}
|
|
127
|
+
className="w-full mt-2 px-3 py-2.5 bg-base-300 border border-base-content/15 rounded-xl text-base-content/30 text-[12px] font-mono leading-relaxed resize-y cursor-default focus-visible:outline-none"
|
|
128
|
+
/>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
|
|
132
|
+
<div>
|
|
133
|
+
<label htmlFor="project-default-model" className="block text-[12px] font-semibold text-base-content/40 mb-2">
|
|
134
|
+
Default Model
|
|
135
|
+
</label>
|
|
136
|
+
<select
|
|
137
|
+
id="project-default-model"
|
|
138
|
+
value={defaultModel ?? ""}
|
|
139
|
+
onChange={function (e) {
|
|
140
|
+
var val = e.target.value || undefined;
|
|
141
|
+
setDefaultModel(val);
|
|
142
|
+
save.markDirty();
|
|
143
|
+
}}
|
|
144
|
+
className="w-full h-9 px-3 bg-base-300 border border-base-content/15 rounded-xl text-base-content text-[13px] focus:border-primary focus-visible:outline-none transition-colors duration-[120ms]"
|
|
145
|
+
>
|
|
146
|
+
<option value="" className="bg-base-300">
|
|
147
|
+
Use global default ({modelLabel(globalModel)})
|
|
148
|
+
</option>
|
|
149
|
+
{CLAUDE_MODELS.map(function (m) {
|
|
150
|
+
return (
|
|
151
|
+
<option key={m.id} value={m.id} className="bg-base-300">
|
|
152
|
+
{m.label}
|
|
153
|
+
</option>
|
|
154
|
+
);
|
|
155
|
+
})}
|
|
156
|
+
</select>
|
|
157
|
+
{defaultModel && (
|
|
158
|
+
<button
|
|
159
|
+
type="button"
|
|
160
|
+
onClick={function () { setDefaultModel(undefined); save.markDirty(); }}
|
|
161
|
+
className="mt-1.5 text-[11px] text-primary/70 hover:text-primary transition-colors"
|
|
162
|
+
>
|
|
163
|
+
Clear override
|
|
164
|
+
</button>
|
|
165
|
+
)}
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<div role="radiogroup" aria-label="Default Effort">
|
|
169
|
+
<div className="text-[12px] font-semibold text-base-content/40 mb-2">Default Effort</div>
|
|
170
|
+
<div className="flex gap-2">
|
|
171
|
+
<button
|
|
172
|
+
role="radio"
|
|
173
|
+
aria-checked={defaultEffort === undefined}
|
|
174
|
+
onClick={function () { setDefaultEffort(undefined); save.markDirty(); }}
|
|
175
|
+
className={
|
|
176
|
+
"flex-1 py-2.5 sm:py-1.5 rounded-lg border text-[12px] transition-colors duration-[120ms] cursor-pointer focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 focus-visible:ring-offset-base-100 " +
|
|
177
|
+
(defaultEffort === undefined
|
|
178
|
+
? "border-primary bg-base-300 text-base-content font-semibold"
|
|
179
|
+
: "border-base-content/15 bg-base-300 text-base-content/40 hover:border-base-content/30 hover:text-base-content/60")
|
|
180
|
+
}
|
|
181
|
+
>
|
|
182
|
+
Global ({globalEffort})
|
|
183
|
+
</button>
|
|
184
|
+
{EFFORT_LEVELS.map(function (e) {
|
|
185
|
+
var active = defaultEffort === e.id;
|
|
186
|
+
return (
|
|
187
|
+
<button
|
|
188
|
+
key={e.id}
|
|
189
|
+
role="radio"
|
|
190
|
+
aria-checked={active}
|
|
191
|
+
onClick={function () { setDefaultEffort(e.id); save.markDirty(); }}
|
|
192
|
+
className={
|
|
193
|
+
"flex-1 py-2.5 sm:py-1.5 rounded-lg border text-[12px] transition-colors duration-[120ms] cursor-pointer focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 focus-visible:ring-offset-base-100 " +
|
|
194
|
+
(active
|
|
195
|
+
? "border-primary bg-base-300 text-base-content font-semibold"
|
|
196
|
+
: "border-base-content/15 bg-base-300 text-base-content/40 hover:border-base-content/30 hover:text-base-content/60")
|
|
197
|
+
}
|
|
198
|
+
>
|
|
199
|
+
{e.label}
|
|
200
|
+
</button>
|
|
201
|
+
);
|
|
202
|
+
})}
|
|
203
|
+
</div>
|
|
204
|
+
</div>
|
|
205
|
+
|
|
206
|
+
<div role="radiogroup" aria-label="Thinking Mode">
|
|
207
|
+
<div className="text-[12px] font-semibold text-base-content/40 mb-2">Thinking Mode</div>
|
|
208
|
+
<div className="flex gap-2">
|
|
209
|
+
<button
|
|
210
|
+
role="radio"
|
|
211
|
+
aria-checked={thinking === undefined}
|
|
212
|
+
onClick={function () { setThinking(undefined); save.markDirty(); }}
|
|
213
|
+
className={
|
|
214
|
+
"flex-1 py-2.5 sm:py-1.5 rounded-lg border text-[12px] transition-colors duration-[120ms] cursor-pointer focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 focus-visible:ring-offset-base-100 " +
|
|
215
|
+
(thinking === undefined
|
|
216
|
+
? "border-primary bg-base-300 text-base-content font-semibold"
|
|
217
|
+
: "border-base-content/15 bg-base-300 text-base-content/40 hover:border-base-content/30 hover:text-base-content/60")
|
|
218
|
+
}
|
|
219
|
+
>
|
|
220
|
+
Global ({thinkingLabel(settings.global.thinking)})
|
|
221
|
+
</button>
|
|
222
|
+
{THINKING_MODES.map(function (t) {
|
|
223
|
+
var active = thinking?.type === t.id;
|
|
224
|
+
return (
|
|
225
|
+
<button
|
|
226
|
+
key={t.id}
|
|
227
|
+
role="radio"
|
|
228
|
+
aria-checked={active}
|
|
229
|
+
onClick={function () {
|
|
230
|
+
var cfg: ThinkingConfig = t.id === "enabled"
|
|
231
|
+
? { type: "enabled", budgetTokens }
|
|
232
|
+
: { type: t.id as "adaptive" | "disabled" };
|
|
233
|
+
setThinking(cfg);
|
|
234
|
+
save.markDirty();
|
|
235
|
+
}}
|
|
236
|
+
className={
|
|
237
|
+
"flex-1 py-2.5 sm:py-1.5 rounded-lg border text-[12px] transition-colors duration-[120ms] cursor-pointer focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 focus-visible:ring-offset-base-100 " +
|
|
238
|
+
(active
|
|
239
|
+
? "border-primary bg-base-300 text-base-content font-semibold"
|
|
240
|
+
: "border-base-content/15 bg-base-300 text-base-content/40 hover:border-base-content/30 hover:text-base-content/60")
|
|
241
|
+
}
|
|
242
|
+
>
|
|
243
|
+
{t.label}
|
|
244
|
+
</button>
|
|
245
|
+
);
|
|
246
|
+
})}
|
|
247
|
+
</div>
|
|
248
|
+
{thinking?.type === "enabled" && (
|
|
249
|
+
<div className="mt-3">
|
|
250
|
+
<label htmlFor="budget-tokens" className="block text-[11px] text-base-content/40 mb-1.5">
|
|
251
|
+
Budget Tokens
|
|
252
|
+
</label>
|
|
253
|
+
<input
|
|
254
|
+
id="budget-tokens"
|
|
255
|
+
type="number"
|
|
256
|
+
min={1000}
|
|
257
|
+
step={1000}
|
|
258
|
+
value={budgetTokens}
|
|
259
|
+
onChange={function (e) {
|
|
260
|
+
var val = parseInt(e.target.value, 10);
|
|
261
|
+
if (!isNaN(val)) {
|
|
262
|
+
setBudgetTokens(val);
|
|
263
|
+
setThinking({ type: "enabled", budgetTokens: val });
|
|
264
|
+
save.markDirty();
|
|
265
|
+
}
|
|
266
|
+
}}
|
|
267
|
+
className="w-48 h-9 px-3 bg-base-300 border border-base-content/15 rounded-xl text-base-content text-[13px] font-mono focus:border-primary focus-visible:outline-none transition-colors duration-[120ms]"
|
|
268
|
+
/>
|
|
269
|
+
</div>
|
|
270
|
+
)}
|
|
271
|
+
</div>
|
|
272
|
+
|
|
273
|
+
<div role="radiogroup" aria-label="Permission Mode">
|
|
274
|
+
<div className="text-[12px] font-semibold text-base-content/40 mb-2">Permission Mode</div>
|
|
275
|
+
<div className="flex gap-2">
|
|
276
|
+
{PERMISSION_MODES.map(function (p) {
|
|
277
|
+
var active = (permissionMode ?? "default") === p.id;
|
|
278
|
+
return (
|
|
279
|
+
<button
|
|
280
|
+
key={p.id}
|
|
281
|
+
role="radio"
|
|
282
|
+
aria-checked={active}
|
|
283
|
+
onClick={function () {
|
|
284
|
+
setPermissionMode(p.id === "default" ? undefined : p.id);
|
|
285
|
+
save.markDirty();
|
|
286
|
+
}}
|
|
287
|
+
className={
|
|
288
|
+
"flex-1 py-2.5 sm:py-1.5 rounded-lg border text-[12px] transition-colors duration-[120ms] cursor-pointer focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 focus-visible:ring-offset-base-100 " +
|
|
289
|
+
(active
|
|
290
|
+
? "border-primary bg-base-300 text-base-content font-semibold"
|
|
291
|
+
: "border-base-content/15 bg-base-300 text-base-content/40 hover:border-base-content/30 hover:text-base-content/60")
|
|
292
|
+
}
|
|
293
|
+
>
|
|
294
|
+
{p.label}
|
|
295
|
+
</button>
|
|
296
|
+
);
|
|
297
|
+
})}
|
|
298
|
+
</div>
|
|
299
|
+
</div>
|
|
300
|
+
|
|
301
|
+
<SaveFooter dirty={save.dirty} saving={save.saving} saveState={save.saveState} onSave={handleSave} />
|
|
302
|
+
</div>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { useState, useEffect, useMemo } from "react";
|
|
2
|
+
import { X, Plus, AlertTriangle } from "lucide-react";
|
|
3
|
+
import { SaveFooter } from "../ui/SaveFooter";
|
|
4
|
+
import { useSaveState } from "../../hooks/useSaveState";
|
|
5
|
+
import { findDuplicateKeys } from "../../utils/findDuplicateKeys";
|
|
6
|
+
import type { ProjectSettings } from "@lattice/shared";
|
|
7
|
+
|
|
8
|
+
interface EnvEntry {
|
|
9
|
+
id: string;
|
|
10
|
+
key: string;
|
|
11
|
+
value: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function genId(): string {
|
|
15
|
+
return Math.random().toString(36).slice(2, 10);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function entriesToEnv(entries: EnvEntry[]): Record<string, string> {
|
|
19
|
+
var env: Record<string, string> = {};
|
|
20
|
+
entries.forEach(function (e) {
|
|
21
|
+
if (e.key.trim()) {
|
|
22
|
+
env[e.key.trim()] = e.value;
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
return env;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function envToEntries(env: Record<string, string>): EnvEntry[] {
|
|
29
|
+
return Object.entries(env).map(function ([k, v]) {
|
|
30
|
+
return { id: genId(), key: k, value: v };
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function ProjectEnvironment({
|
|
35
|
+
settings,
|
|
36
|
+
updateSection,
|
|
37
|
+
}: {
|
|
38
|
+
settings: ProjectSettings;
|
|
39
|
+
updateSection: (section: string, data: Record<string, unknown>) => void;
|
|
40
|
+
}) {
|
|
41
|
+
var globalEnv = settings.global.env ?? {};
|
|
42
|
+
var globalEntries = Object.entries(globalEnv);
|
|
43
|
+
|
|
44
|
+
var [entries, setEntries] = useState<EnvEntry[]>(function () {
|
|
45
|
+
return envToEntries(settings.env ?? {});
|
|
46
|
+
});
|
|
47
|
+
var save = useSaveState();
|
|
48
|
+
|
|
49
|
+
useEffect(function () {
|
|
50
|
+
if (save.saving) {
|
|
51
|
+
save.confirmSave();
|
|
52
|
+
} else {
|
|
53
|
+
setEntries(envToEntries(settings.env ?? {}));
|
|
54
|
+
save.resetFromServer();
|
|
55
|
+
}
|
|
56
|
+
}, [settings]);
|
|
57
|
+
|
|
58
|
+
var globalKeySet = useMemo(function () {
|
|
59
|
+
return new Set(Object.keys(globalEnv));
|
|
60
|
+
}, [globalEnv]);
|
|
61
|
+
|
|
62
|
+
var duplicateKeys = useMemo(function () {
|
|
63
|
+
return findDuplicateKeys(entries);
|
|
64
|
+
}, [entries]);
|
|
65
|
+
var hasDuplicates = duplicateKeys.size > 0;
|
|
66
|
+
|
|
67
|
+
function handleAddRow() {
|
|
68
|
+
setEntries(function (prev) {
|
|
69
|
+
return [...prev, { id: genId(), key: "", value: "" }];
|
|
70
|
+
});
|
|
71
|
+
save.markDirty();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function handleDelete(id: string) {
|
|
75
|
+
setEntries(function (prev) {
|
|
76
|
+
return prev.filter(function (e) { return e.id !== id; });
|
|
77
|
+
});
|
|
78
|
+
save.markDirty();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function handleKeyChange(id: string, key: string) {
|
|
82
|
+
setEntries(function (prev) {
|
|
83
|
+
return prev.map(function (e) {
|
|
84
|
+
return e.id === id ? { ...e, key } : e;
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
save.markDirty();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function handleValueChange(id: string, value: string) {
|
|
91
|
+
setEntries(function (prev) {
|
|
92
|
+
return prev.map(function (e) {
|
|
93
|
+
return e.id === id ? { ...e, value } : e;
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
save.markDirty();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function handleSave() {
|
|
100
|
+
save.startSave();
|
|
101
|
+
updateSection("environment", { env: entriesToEnv(entries) });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
var inputClass = "w-full h-9 sm:h-7 px-3 bg-base-300 border border-base-content/15 rounded-xl text-base-content font-mono text-[12px] focus:border-primary focus-visible:outline-none transition-colors duration-[120ms]";
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div className="py-2">
|
|
108
|
+
<div className="mb-6">
|
|
109
|
+
<h2 className="text-[12px] font-semibold text-base-content/40 mb-3">
|
|
110
|
+
Global Variables
|
|
111
|
+
</h2>
|
|
112
|
+
{globalEntries.length === 0 && (
|
|
113
|
+
<div className="py-4 text-center text-[13px] text-base-content/30">
|
|
114
|
+
No global environment variables.
|
|
115
|
+
</div>
|
|
116
|
+
)}
|
|
117
|
+
{globalEntries.length > 0 && (
|
|
118
|
+
<div className="flex flex-col gap-1.5">
|
|
119
|
+
{globalEntries.map(function ([k, v]) {
|
|
120
|
+
return (
|
|
121
|
+
<div
|
|
122
|
+
key={k}
|
|
123
|
+
className="flex flex-col sm:grid sm:grid-cols-[1fr_1fr_auto] gap-1.5 sm:items-center"
|
|
124
|
+
>
|
|
125
|
+
<div className="h-9 sm:h-7 px-3 bg-base-300/50 border border-base-content/10 rounded-xl flex items-center font-mono text-[12px] text-base-content/40">
|
|
126
|
+
{k}
|
|
127
|
+
</div>
|
|
128
|
+
<div className="flex gap-1.5 items-center">
|
|
129
|
+
<div className="h-9 sm:h-7 px-3 bg-base-300/50 border border-base-content/10 rounded-xl flex items-center font-mono text-[12px] text-base-content/40 w-full">
|
|
130
|
+
{v}
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
<span className="text-[10px] uppercase tracking-wider text-base-content/30 w-7 text-center hidden sm:block">
|
|
134
|
+
global
|
|
135
|
+
</span>
|
|
136
|
+
</div>
|
|
137
|
+
);
|
|
138
|
+
})}
|
|
139
|
+
</div>
|
|
140
|
+
)}
|
|
141
|
+
</div>
|
|
142
|
+
|
|
143
|
+
<div>
|
|
144
|
+
<h2 className="text-[12px] font-semibold text-base-content/40 mb-3">
|
|
145
|
+
Project Variables
|
|
146
|
+
</h2>
|
|
147
|
+
<div className="flex flex-col gap-3 sm:gap-1.5 mb-3">
|
|
148
|
+
{entries.length === 0 && (
|
|
149
|
+
<div className="py-4 text-center text-[13px] text-base-content/30">
|
|
150
|
+
No project environment variables.
|
|
151
|
+
</div>
|
|
152
|
+
)}
|
|
153
|
+
{entries.map(function (entry, idx) {
|
|
154
|
+
var isDupe = entry.key.trim() !== "" && duplicateKeys.has(entry.key.trim());
|
|
155
|
+
var overridesGlobal = entry.key.trim() !== "" && globalKeySet.has(entry.key.trim());
|
|
156
|
+
return (
|
|
157
|
+
<div
|
|
158
|
+
key={entry.id}
|
|
159
|
+
className="flex flex-col sm:grid sm:grid-cols-[1fr_1fr_auto] gap-1.5 sm:items-center"
|
|
160
|
+
>
|
|
161
|
+
<div className="relative">
|
|
162
|
+
<input
|
|
163
|
+
type="text"
|
|
164
|
+
value={entry.key}
|
|
165
|
+
onChange={function (e) { handleKeyChange(entry.id, e.target.value); }}
|
|
166
|
+
placeholder="VARIABLE_NAME"
|
|
167
|
+
aria-label={"Variable name for row " + (idx + 1)}
|
|
168
|
+
aria-invalid={isDupe}
|
|
169
|
+
className={
|
|
170
|
+
"w-full h-9 sm:h-7 px-3 bg-base-300 border rounded-xl text-base-content font-mono text-[12px] focus:border-primary focus-visible:outline-none transition-colors duration-[120ms] " +
|
|
171
|
+
(isDupe ? "border-warning" : "border-base-content/15")
|
|
172
|
+
}
|
|
173
|
+
/>
|
|
174
|
+
{isDupe && (
|
|
175
|
+
<div className="flex items-center gap-1 mt-0.5 text-[10px] text-warning sm:absolute sm:-bottom-3.5 sm:left-0" role="alert">
|
|
176
|
+
<AlertTriangle size={9} />
|
|
177
|
+
Duplicate key
|
|
178
|
+
</div>
|
|
179
|
+
)}
|
|
180
|
+
{!isDupe && overridesGlobal && (
|
|
181
|
+
<div className="flex items-center gap-1 mt-0.5 text-[10px] text-warning/70 sm:absolute sm:-bottom-3.5 sm:left-0">
|
|
182
|
+
overrides global
|
|
183
|
+
</div>
|
|
184
|
+
)}
|
|
185
|
+
</div>
|
|
186
|
+
<div className="flex gap-1.5 items-center">
|
|
187
|
+
<input
|
|
188
|
+
type="text"
|
|
189
|
+
value={entry.value}
|
|
190
|
+
onChange={function (e) { handleValueChange(entry.id, e.target.value); }}
|
|
191
|
+
placeholder="value"
|
|
192
|
+
aria-label={"Value for " + (entry.key || "row " + (idx + 1))}
|
|
193
|
+
className={inputClass}
|
|
194
|
+
/>
|
|
195
|
+
<button
|
|
196
|
+
onClick={function () { handleDelete(entry.id); }}
|
|
197
|
+
aria-label={"Delete " + (entry.key || "row " + (idx + 1))}
|
|
198
|
+
title="Delete"
|
|
199
|
+
className="btn btn-ghost btn-xs btn-square text-base-content/30 hover:text-error w-9 h-9 flex-shrink-0 sm:hidden focus-visible:ring-2 focus-visible:ring-primary"
|
|
200
|
+
>
|
|
201
|
+
<X size={14} />
|
|
202
|
+
</button>
|
|
203
|
+
</div>
|
|
204
|
+
<button
|
|
205
|
+
onClick={function () { handleDelete(entry.id); }}
|
|
206
|
+
aria-label={"Delete " + (entry.key || "row " + (idx + 1))}
|
|
207
|
+
title="Delete"
|
|
208
|
+
className="btn btn-ghost btn-xs btn-square text-base-content/30 hover:text-error w-7 h-7 hidden sm:flex focus-visible:ring-2 focus-visible:ring-primary"
|
|
209
|
+
>
|
|
210
|
+
<X size={12} />
|
|
211
|
+
</button>
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
})}
|
|
215
|
+
</div>
|
|
216
|
+
|
|
217
|
+
<button
|
|
218
|
+
onClick={handleAddRow}
|
|
219
|
+
className="flex items-center gap-1.5 px-3 py-2.5 sm:py-1.5 rounded-xl border border-dashed border-base-content/20 bg-transparent text-base-content/40 text-[12px] hover:text-base-content/60 hover:border-base-content/30 transition-colors duration-[120ms] mb-5 cursor-pointer focus-visible:ring-2 focus-visible:ring-primary focus-visible:ring-offset-1 focus-visible:ring-offset-base-100"
|
|
220
|
+
>
|
|
221
|
+
<Plus size={12} />
|
|
222
|
+
Add Variable
|
|
223
|
+
</button>
|
|
224
|
+
|
|
225
|
+
<SaveFooter
|
|
226
|
+
dirty={save.dirty}
|
|
227
|
+
saving={save.saving}
|
|
228
|
+
saveState={save.saveState}
|
|
229
|
+
onSave={handleSave}
|
|
230
|
+
extraStatus={hasDuplicates ? "Duplicate keys will be merged" : undefined}
|
|
231
|
+
/>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
);
|
|
235
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useState, useEffect } from "react";
|
|
2
|
+
import { IconPicker } from "../ui/IconPicker";
|
|
3
|
+
import { SaveFooter } from "../ui/SaveFooter";
|
|
4
|
+
import { useSaveState } from "../../hooks/useSaveState";
|
|
5
|
+
import type { ProjectSettings, ProjectIcon } from "@lattice/shared";
|
|
6
|
+
|
|
7
|
+
interface ProjectGeneralProps {
|
|
8
|
+
settings: ProjectSettings;
|
|
9
|
+
updateSection: (section: string, data: Record<string, unknown>) => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function ProjectGeneral({ settings, updateSection }: ProjectGeneralProps) {
|
|
13
|
+
var [title, setTitle] = useState(settings.title);
|
|
14
|
+
var [icon, setIcon] = useState<ProjectIcon | undefined>(settings.icon);
|
|
15
|
+
var save = useSaveState();
|
|
16
|
+
|
|
17
|
+
useEffect(function () {
|
|
18
|
+
if (save.saving) {
|
|
19
|
+
save.confirmSave();
|
|
20
|
+
} else {
|
|
21
|
+
setTitle(settings.title);
|
|
22
|
+
setIcon(settings.icon);
|
|
23
|
+
save.resetFromServer();
|
|
24
|
+
}
|
|
25
|
+
}, [settings]);
|
|
26
|
+
|
|
27
|
+
function handleTitleChange(value: string) {
|
|
28
|
+
setTitle(value);
|
|
29
|
+
save.markDirty();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function handleIconChange(value: ProjectIcon) {
|
|
33
|
+
setIcon(value);
|
|
34
|
+
save.markDirty();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function handleSave() {
|
|
38
|
+
save.startSave();
|
|
39
|
+
updateSection("general", { title, icon });
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div className="py-2">
|
|
44
|
+
<div className="mb-5">
|
|
45
|
+
<label htmlFor="project-title" className="block text-[12px] font-semibold text-base-content/40 mb-2">
|
|
46
|
+
Display Name
|
|
47
|
+
</label>
|
|
48
|
+
<input
|
|
49
|
+
id="project-title"
|
|
50
|
+
type="text"
|
|
51
|
+
value={title}
|
|
52
|
+
onChange={function (e) { handleTitleChange(e.target.value); }}
|
|
53
|
+
className="w-full h-9 px-3 bg-base-300 border border-base-content/15 rounded-xl text-base-content text-[13px] focus:border-primary focus-visible:outline-none transition-colors duration-[120ms]"
|
|
54
|
+
/>
|
|
55
|
+
</div>
|
|
56
|
+
|
|
57
|
+
<div className="mb-5">
|
|
58
|
+
<div className="block text-[12px] font-semibold text-base-content/40 mb-2">
|
|
59
|
+
Icon
|
|
60
|
+
</div>
|
|
61
|
+
<IconPicker value={icon} onChange={handleIconChange} />
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div className="mb-6">
|
|
65
|
+
<div className="block text-[12px] font-semibold text-base-content/40 mb-2">
|
|
66
|
+
Project Path
|
|
67
|
+
</div>
|
|
68
|
+
<div className="w-full h-9 px-3 bg-base-300 border border-base-content/15 rounded-xl text-base-content/60 text-[12px] font-mono flex items-center select-all">
|
|
69
|
+
{settings.path}
|
|
70
|
+
</div>
|
|
71
|
+
</div>
|
|
72
|
+
|
|
73
|
+
<SaveFooter dirty={save.dirty} saving={save.saving} saveState={save.saveState} onSave={handleSave} />
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
}
|