@cryptiklemur/lattice 1.3.0 → 1.4.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/bun.lock +705 -2
- package/client/index.html +1 -13
- package/client/package.json +6 -1
- package/client/src/App.tsx +2 -0
- package/client/src/commands.ts +36 -0
- package/client/src/components/chat/AttachmentChips.tsx +116 -0
- package/client/src/components/chat/ChatInput.tsx +250 -73
- package/client/src/components/chat/ChatView.tsx +242 -10
- package/client/src/components/chat/CommandPalette.tsx +162 -0
- package/client/src/components/chat/Message.tsx +23 -2
- package/client/src/components/chat/PromptQuestion.tsx +271 -0
- package/client/src/components/chat/TodoCard.tsx +57 -0
- package/client/src/components/chat/ToolResultRenderer.tsx +2 -1
- package/client/src/components/chat/VoiceRecorder.tsx +85 -0
- package/client/src/components/project-settings/ProjectMemory.tsx +12 -2
- package/client/src/components/project-settings/ProjectNotifications.tsx +48 -0
- package/client/src/components/project-settings/ProjectRules.tsx +10 -1
- package/client/src/components/project-settings/ProjectSettingsView.tsx +6 -0
- package/client/src/components/settings/Appearance.tsx +1 -0
- package/client/src/components/settings/ClaudeSettings.tsx +10 -0
- package/client/src/components/settings/Editor.tsx +123 -0
- package/client/src/components/settings/GlobalMcp.tsx +10 -1
- package/client/src/components/settings/GlobalMemory.tsx +19 -0
- package/client/src/components/settings/GlobalRules.tsx +149 -0
- package/client/src/components/settings/GlobalSkills.tsx +10 -0
- package/client/src/components/settings/Notifications.tsx +88 -0
- package/client/src/components/settings/SettingsView.tsx +12 -0
- package/client/src/components/settings/skill-shared.tsx +2 -1
- package/client/src/components/setup/SetupWizard.tsx +1 -1
- package/client/src/components/sidebar/NodeSettingsModal.tsx +23 -1
- package/client/src/components/sidebar/ProjectDropdown.tsx +176 -27
- package/client/src/components/sidebar/SettingsSidebar.tsx +11 -1
- package/client/src/components/sidebar/Sidebar.tsx +35 -2
- package/client/src/components/sidebar/UserIsland.tsx +18 -7
- package/client/src/components/ui/UpdatePrompt.tsx +47 -0
- package/client/src/components/workspace/FileBrowser.tsx +174 -0
- package/client/src/components/workspace/FileTree.tsx +129 -0
- package/client/src/components/workspace/FileViewer.tsx +211 -0
- package/client/src/components/workspace/NoteCard.tsx +119 -0
- package/client/src/components/workspace/NotesView.tsx +102 -0
- package/client/src/components/workspace/ScheduledTasksView.tsx +117 -0
- package/client/src/components/workspace/SplitPane.tsx +81 -0
- package/client/src/components/workspace/TabBar.tsx +185 -0
- package/client/src/components/workspace/TaskCard.tsx +158 -0
- package/client/src/components/workspace/TaskEditModal.tsx +114 -0
- package/client/src/components/{panels/Terminal.tsx → workspace/TerminalInstance.tsx} +50 -7
- package/client/src/components/workspace/TerminalView.tsx +110 -0
- package/client/src/components/workspace/WorkspaceView.tsx +116 -0
- package/client/src/hooks/useAttachments.ts +280 -0
- package/client/src/hooks/useEditorConfig.ts +28 -0
- package/client/src/hooks/useIdleDetection.ts +44 -0
- package/client/src/hooks/useInstallPrompt.ts +53 -0
- package/client/src/hooks/useNotifications.ts +54 -0
- package/client/src/hooks/useOnline.ts +6 -0
- package/client/src/hooks/useSession.ts +110 -4
- package/client/src/hooks/useSpinnerVerb.ts +36 -0
- package/client/src/hooks/useSwipeDrawer.ts +275 -0
- package/client/src/hooks/useVoiceRecorder.ts +123 -0
- package/client/src/hooks/useWorkspace.ts +48 -0
- package/client/src/providers/WebSocketProvider.tsx +18 -0
- package/client/src/router.tsx +48 -20
- package/client/src/stores/session.ts +136 -0
- package/client/src/stores/sidebar.ts +3 -2
- package/client/src/stores/workspace.ts +254 -0
- package/client/src/styles/global.css +123 -0
- package/client/src/utils/editorUrl.ts +62 -0
- package/client/vite.config.ts +53 -1
- package/package.json +1 -1
- package/server/src/daemon.ts +11 -1
- package/server/src/features/scheduler.ts +23 -0
- package/server/src/features/sticky-notes.ts +5 -3
- package/server/src/handlers/attachment.ts +172 -0
- package/server/src/handlers/chat.ts +43 -2
- package/server/src/handlers/editor.ts +40 -0
- package/server/src/handlers/fs.ts +10 -2
- package/server/src/handlers/memory.ts +3 -0
- package/server/src/handlers/notes.ts +4 -2
- package/server/src/handlers/scheduler.ts +18 -1
- package/server/src/handlers/session.ts +14 -8
- package/server/src/handlers/settings.ts +37 -2
- package/server/src/handlers/terminal.ts +13 -6
- package/server/src/project/pty-worker.cjs +83 -0
- package/server/src/project/sdk-bridge.ts +266 -11
- package/server/src/project/terminal.ts +78 -34
- package/shared/src/messages.ts +145 -4
- package/shared/src/models.ts +27 -1
- package/shared/src/project-settings.ts +1 -1
- package/tp.js +19 -0
- package/client/public/manifest.json +0 -24
- package/client/public/sw.js +0 -61
- package/client/src/components/panels/FileBrowser.tsx +0 -241
- package/client/src/components/panels/StickyNotes.tsx +0 -187
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
import { useState, useRef, useEffect } from "react";
|
|
2
|
+
import { Check, Circle, CheckCircle2, MessageCircleQuestion, ChevronDown, Send } from "lucide-react";
|
|
3
|
+
import type { HistoryMessage } from "@lattice/shared";
|
|
4
|
+
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
5
|
+
import { resolvePromptQuestion } from "../../stores/session";
|
|
6
|
+
|
|
7
|
+
interface PromptQuestionProps {
|
|
8
|
+
message: HistoryMessage;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
var LETTERS = ["A", "B", "C", "D"];
|
|
12
|
+
|
|
13
|
+
export function PromptQuestion(props: PromptQuestionProps) {
|
|
14
|
+
var msg = props.message;
|
|
15
|
+
var { send } = useWebSocket();
|
|
16
|
+
var [otherText, setOtherText] = useState("");
|
|
17
|
+
var [selectedMulti, setSelectedMulti] = useState<Set<string>>(new Set());
|
|
18
|
+
var [focusIndex, setFocusIndex] = useState(-1);
|
|
19
|
+
var [expanded, setExpanded] = useState(false);
|
|
20
|
+
var optionsRef = useRef<HTMLDivElement>(null);
|
|
21
|
+
|
|
22
|
+
var questions = msg.promptQuestions || [];
|
|
23
|
+
var answers = msg.promptAnswers;
|
|
24
|
+
var status = msg.promptStatus || "pending";
|
|
25
|
+
var requestId = msg.toolId || "";
|
|
26
|
+
|
|
27
|
+
useEffect(function () {
|
|
28
|
+
if (status === "pending" && optionsRef.current) {
|
|
29
|
+
var firstBtn = optionsRef.current.querySelector("button");
|
|
30
|
+
if (firstBtn) firstBtn.focus();
|
|
31
|
+
}
|
|
32
|
+
}, [status]);
|
|
33
|
+
|
|
34
|
+
function submitAnswer(questionText: string, answer: string) {
|
|
35
|
+
var answerMap: Record<string, string> = {};
|
|
36
|
+
answerMap[questionText] = answer;
|
|
37
|
+
resolvePromptQuestion(requestId, answerMap);
|
|
38
|
+
send({
|
|
39
|
+
type: "chat:prompt_response",
|
|
40
|
+
requestId: requestId,
|
|
41
|
+
answers: answerMap,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function submitMultiSelect(questionText: string) {
|
|
46
|
+
var parts = Array.from(selectedMulti);
|
|
47
|
+
if (otherText.trim()) parts.push(otherText.trim());
|
|
48
|
+
submitAnswer(questionText, parts.join(", "));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function submitOther(questionText: string) {
|
|
52
|
+
if (!otherText.trim()) return;
|
|
53
|
+
submitAnswer(questionText, otherText.trim());
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function handleKeyDown(e: React.KeyboardEvent, optionCount: number, q: { question: string; multiSelect: boolean }, options: Array<{ label: string }>) {
|
|
57
|
+
if (e.key === "ArrowDown" || e.key === "ArrowRight") {
|
|
58
|
+
e.preventDefault();
|
|
59
|
+
var next = focusIndex < optionCount - 1 ? focusIndex + 1 : 0;
|
|
60
|
+
setFocusIndex(next);
|
|
61
|
+
var btns = optionsRef.current?.querySelectorAll("[data-option]");
|
|
62
|
+
if (btns && btns[next]) (btns[next] as HTMLElement).focus();
|
|
63
|
+
}
|
|
64
|
+
if (e.key === "ArrowUp" || e.key === "ArrowLeft") {
|
|
65
|
+
e.preventDefault();
|
|
66
|
+
var prev = focusIndex > 0 ? focusIndex - 1 : optionCount - 1;
|
|
67
|
+
setFocusIndex(prev);
|
|
68
|
+
var btns = optionsRef.current?.querySelectorAll("[data-option]");
|
|
69
|
+
if (btns && btns[prev]) (btns[prev] as HTMLElement).focus();
|
|
70
|
+
}
|
|
71
|
+
if (e.key === "Enter" && !q.multiSelect && focusIndex >= 0 && focusIndex < options.length) {
|
|
72
|
+
e.preventDefault();
|
|
73
|
+
submitAnswer(q.question, options[focusIndex].label);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (status === "answered" && answers) {
|
|
78
|
+
var firstAnswer = Object.entries(answers)[0];
|
|
79
|
+
var answeredQuestion = questions.length > 0 ? questions[0] : null;
|
|
80
|
+
return (
|
|
81
|
+
<div className="px-5 py-1.5" aria-live="polite">
|
|
82
|
+
<div
|
|
83
|
+
className="rounded-xl border border-success/15 bg-base-300/40 overflow-hidden cursor-pointer transition-colors hover:bg-base-300/60"
|
|
84
|
+
onClick={function () { setExpanded(!expanded); }}
|
|
85
|
+
>
|
|
86
|
+
<div className="flex items-center gap-2.5 px-4 py-2.5">
|
|
87
|
+
<CheckCircle2 size={15} className="text-success/60 flex-shrink-0" />
|
|
88
|
+
{answeredQuestion && (
|
|
89
|
+
<span className="text-[10px] font-mono font-semibold uppercase tracking-wider text-base-content/30">{answeredQuestion.header}</span>
|
|
90
|
+
)}
|
|
91
|
+
<span className="text-[12px] text-base-content/50">{firstAnswer ? firstAnswer[0] : ""}</span>
|
|
92
|
+
<span className="flex-1" />
|
|
93
|
+
<span className="text-[12px] font-medium text-base-content/70">{firstAnswer ? firstAnswer[1] : ""}</span>
|
|
94
|
+
<ChevronDown
|
|
95
|
+
size={12}
|
|
96
|
+
className={"text-base-content/25 transition-transform duration-200 " + (expanded ? "rotate-180" : "")}
|
|
97
|
+
/>
|
|
98
|
+
</div>
|
|
99
|
+
|
|
100
|
+
{expanded && answeredQuestion && (
|
|
101
|
+
<div className="px-4 pb-3 border-t border-base-content/5">
|
|
102
|
+
<div className="flex flex-col gap-1 pt-2">
|
|
103
|
+
{answeredQuestion.options.map(function (opt, oi) {
|
|
104
|
+
var isChosen = firstAnswer && firstAnswer[1] === opt.label;
|
|
105
|
+
return (
|
|
106
|
+
<div
|
|
107
|
+
key={oi}
|
|
108
|
+
className={
|
|
109
|
+
"flex items-center gap-2.5 px-3 py-1.5 rounded-lg text-[12px] " +
|
|
110
|
+
(isChosen ? "bg-success/8 text-base-content/70" : "text-base-content/30")
|
|
111
|
+
}
|
|
112
|
+
>
|
|
113
|
+
<span className={
|
|
114
|
+
"w-5 h-5 rounded-md flex items-center justify-center text-[10px] font-mono font-bold flex-shrink-0 " +
|
|
115
|
+
(isChosen ? "bg-success/20 text-success/80" : "bg-base-content/5 text-base-content/20")
|
|
116
|
+
}>
|
|
117
|
+
{LETTERS[oi] || ""}
|
|
118
|
+
</span>
|
|
119
|
+
<span className={isChosen ? "font-medium" : ""}>{opt.label}</span>
|
|
120
|
+
{isChosen && <Check size={12} className="text-success/60 ml-auto" />}
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
})}
|
|
124
|
+
</div>
|
|
125
|
+
</div>
|
|
126
|
+
)}
|
|
127
|
+
</div>
|
|
128
|
+
</div>
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (status === "timed_out") {
|
|
133
|
+
return (
|
|
134
|
+
<div className="px-5 py-1.5">
|
|
135
|
+
<div className="flex items-center gap-2.5 px-4 py-2.5 rounded-xl bg-warning/5 border border-warning/15 text-[12px] text-warning/50 font-mono">
|
|
136
|
+
<Circle size={13} className="flex-shrink-0" />
|
|
137
|
+
Prompt timed out — no response sent
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<div className="px-5 py-2" aria-live="polite">
|
|
145
|
+
{questions.map(function (q, qi) {
|
|
146
|
+
return (
|
|
147
|
+
<div
|
|
148
|
+
key={qi}
|
|
149
|
+
className="rounded-xl border border-primary/15 bg-base-300/50 overflow-hidden shadow-[0_2px_8px_oklch(from_var(--color-primary)_l_c_h/0.06)]"
|
|
150
|
+
>
|
|
151
|
+
<div className="flex items-center gap-2.5 px-4 py-2.5 border-b border-base-content/5 bg-base-content/[0.02]">
|
|
152
|
+
<MessageCircleQuestion size={14} className="text-primary/40 flex-shrink-0" />
|
|
153
|
+
<span className="text-[10px] font-mono font-bold uppercase tracking-widest text-primary/35">{q.header}</span>
|
|
154
|
+
<span className="flex-1" />
|
|
155
|
+
<span className="text-[9px] font-mono text-base-content/20">select one</span>
|
|
156
|
+
</div>
|
|
157
|
+
|
|
158
|
+
<div className="px-4 py-3.5">
|
|
159
|
+
<div className="text-[13px] text-base-content/80 mb-3.5 leading-relaxed">{q.question}</div>
|
|
160
|
+
|
|
161
|
+
<div
|
|
162
|
+
ref={optionsRef}
|
|
163
|
+
className="flex flex-col gap-1.5"
|
|
164
|
+
role={q.multiSelect ? "group" : "radiogroup"}
|
|
165
|
+
aria-label={q.question}
|
|
166
|
+
onKeyDown={function (e) { handleKeyDown(e, q.options.length, q, q.options); }}
|
|
167
|
+
>
|
|
168
|
+
{q.options.map(function (opt, oi) {
|
|
169
|
+
var isSelected = selectedMulti.has(opt.label);
|
|
170
|
+
var isFocused = focusIndex === oi;
|
|
171
|
+
return (
|
|
172
|
+
<button
|
|
173
|
+
key={oi}
|
|
174
|
+
data-option={oi}
|
|
175
|
+
role={q.multiSelect ? "checkbox" : "radio"}
|
|
176
|
+
aria-checked={isSelected}
|
|
177
|
+
tabIndex={q.multiSelect ? 0 : (isFocused || (focusIndex === -1 && oi === 0) ? 0 : -1)}
|
|
178
|
+
onFocus={function () { setFocusIndex(oi); }}
|
|
179
|
+
onClick={function () {
|
|
180
|
+
if (q.multiSelect) {
|
|
181
|
+
setSelectedMulti(function (prev) {
|
|
182
|
+
var next = new Set(prev);
|
|
183
|
+
if (next.has(opt.label)) {
|
|
184
|
+
next.delete(opt.label);
|
|
185
|
+
} else {
|
|
186
|
+
next.add(opt.label);
|
|
187
|
+
}
|
|
188
|
+
return next;
|
|
189
|
+
});
|
|
190
|
+
} else {
|
|
191
|
+
submitAnswer(q.question, opt.label);
|
|
192
|
+
}
|
|
193
|
+
}}
|
|
194
|
+
className={
|
|
195
|
+
"group text-left flex items-start gap-3 px-3.5 py-2.5 rounded-lg text-[12px] cursor-pointer transition-all duration-150 outline-none " +
|
|
196
|
+
(isSelected
|
|
197
|
+
? "bg-primary/10 border border-primary/25 text-base-content/85 shadow-[0_0_0_1px_oklch(from_var(--color-primary)_l_c_h/0.1)]"
|
|
198
|
+
: "bg-base-content/[0.02] border border-base-content/8 text-base-content/60 hover:bg-base-content/5 hover:border-base-content/15 hover:text-base-content/75") +
|
|
199
|
+
" focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-offset-1 focus-visible:ring-offset-base-300"
|
|
200
|
+
}
|
|
201
|
+
>
|
|
202
|
+
<span className={
|
|
203
|
+
"w-5 h-5 rounded-md flex items-center justify-center text-[10px] font-mono font-bold flex-shrink-0 mt-px transition-colors duration-150 " +
|
|
204
|
+
(isSelected
|
|
205
|
+
? "bg-primary/25 text-primary"
|
|
206
|
+
: "bg-base-content/6 text-base-content/30 group-hover:bg-base-content/10 group-hover:text-base-content/45")
|
|
207
|
+
}>
|
|
208
|
+
{q.multiSelect
|
|
209
|
+
? (isSelected ? <Check size={11} /> : LETTERS[oi] || "")
|
|
210
|
+
: LETTERS[oi] || ""
|
|
211
|
+
}
|
|
212
|
+
</span>
|
|
213
|
+
<div className="flex-1 min-w-0">
|
|
214
|
+
<div className="font-medium leading-snug">{opt.label}</div>
|
|
215
|
+
{opt.description && (
|
|
216
|
+
<div className="text-[11px] text-base-content/35 mt-0.5 leading-relaxed group-hover:text-base-content/45 transition-colors duration-150">{opt.description}</div>
|
|
217
|
+
)}
|
|
218
|
+
</div>
|
|
219
|
+
</button>
|
|
220
|
+
);
|
|
221
|
+
})}
|
|
222
|
+
|
|
223
|
+
<div className="flex items-center gap-2 mt-1.5 pt-1.5 border-t border-base-content/5">
|
|
224
|
+
<input
|
|
225
|
+
type="text"
|
|
226
|
+
placeholder="Other..."
|
|
227
|
+
value={otherText}
|
|
228
|
+
onChange={function (e) { setOtherText(e.target.value); }}
|
|
229
|
+
onKeyDown={function (e) {
|
|
230
|
+
if (e.key === "Enter" && !q.multiSelect) {
|
|
231
|
+
submitOther(q.question);
|
|
232
|
+
}
|
|
233
|
+
}}
|
|
234
|
+
aria-label="Custom answer"
|
|
235
|
+
className="flex-1 bg-base-content/[0.02] border border-base-content/8 rounded-lg px-3 py-2 text-[12px] text-base-content/60 placeholder:text-base-content/20 outline-none focus:border-primary/30 focus:bg-base-content/[0.04] transition-colors duration-150"
|
|
236
|
+
/>
|
|
237
|
+
{!q.multiSelect && otherText.trim() && (
|
|
238
|
+
<button
|
|
239
|
+
onClick={function () { submitOther(q.question); }}
|
|
240
|
+
className="flex items-center gap-1.5 px-3 py-2 rounded-lg bg-primary/15 text-primary text-[11px] font-medium hover:bg-primary/25 transition-colors duration-150 cursor-pointer focus-visible:ring-2 focus-visible:ring-primary/40"
|
|
241
|
+
aria-label="Submit custom answer"
|
|
242
|
+
>
|
|
243
|
+
<Send size={10} />
|
|
244
|
+
Send
|
|
245
|
+
</button>
|
|
246
|
+
)}
|
|
247
|
+
</div>
|
|
248
|
+
|
|
249
|
+
{q.multiSelect && (
|
|
250
|
+
<button
|
|
251
|
+
onClick={function () { submitMultiSelect(q.question); }}
|
|
252
|
+
disabled={selectedMulti.size === 0 && !otherText.trim()}
|
|
253
|
+
className={
|
|
254
|
+
"mt-1 flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg text-[12px] font-medium transition-all duration-150 cursor-pointer " +
|
|
255
|
+
(selectedMulti.size > 0 || otherText.trim()
|
|
256
|
+
? "bg-primary text-primary-content hover:bg-primary/85 shadow-sm focus-visible:ring-2 focus-visible:ring-primary/40 focus-visible:ring-offset-2 focus-visible:ring-offset-base-300"
|
|
257
|
+
: "bg-base-content/5 text-base-content/20 cursor-not-allowed")
|
|
258
|
+
}
|
|
259
|
+
>
|
|
260
|
+
<Send size={12} />
|
|
261
|
+
Submit ({selectedMulti.size} selected)
|
|
262
|
+
</button>
|
|
263
|
+
)}
|
|
264
|
+
</div>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
);
|
|
268
|
+
})}
|
|
269
|
+
</div>
|
|
270
|
+
);
|
|
271
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Circle, CircleDot, CheckCircle2, ListTodo } from "lucide-react";
|
|
2
|
+
import type { HistoryMessage } from "@lattice/shared";
|
|
3
|
+
|
|
4
|
+
interface TodoCardProps {
|
|
5
|
+
message: HistoryMessage;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function StatusIcon(props: { status: string }) {
|
|
9
|
+
if (props.status === "completed") {
|
|
10
|
+
return <CheckCircle2 size={13} className="text-success/70 flex-shrink-0" />;
|
|
11
|
+
}
|
|
12
|
+
if (props.status === "in_progress") {
|
|
13
|
+
return <CircleDot size={13} className="text-primary/70 flex-shrink-0 animate-pulse" />;
|
|
14
|
+
}
|
|
15
|
+
return <Circle size={13} className="text-base-content/25 flex-shrink-0" />;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function TodoCard(props: TodoCardProps) {
|
|
19
|
+
var todos = props.message.todos || [];
|
|
20
|
+
if (todos.length === 0) return null;
|
|
21
|
+
|
|
22
|
+
var completed = todos.filter(function (t) { return t.status === "completed"; }).length;
|
|
23
|
+
var total = todos.length;
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="px-5 py-2">
|
|
27
|
+
<div className="rounded-xl border border-base-content/8 bg-base-300/60 overflow-hidden shadow-sm">
|
|
28
|
+
<div className="flex items-center gap-2 px-4 py-2 border-b border-base-content/6 bg-base-content/3">
|
|
29
|
+
<ListTodo size={14} className="text-base-content/40 flex-shrink-0" />
|
|
30
|
+
<span className="text-[10px] font-mono font-semibold uppercase tracking-wider text-base-content/35">Tasks</span>
|
|
31
|
+
<span className="text-[10px] font-mono text-base-content/25">{completed}/{total}</span>
|
|
32
|
+
</div>
|
|
33
|
+
|
|
34
|
+
<div className="px-3 py-2">
|
|
35
|
+
{todos.map(function (todo) {
|
|
36
|
+
return (
|
|
37
|
+
<div
|
|
38
|
+
key={todo.id}
|
|
39
|
+
className={
|
|
40
|
+
"flex items-start gap-2 px-2 py-1.5 rounded-md text-[12px] " +
|
|
41
|
+
(todo.status === "completed" ? "text-base-content/35" : "text-base-content/65")
|
|
42
|
+
}
|
|
43
|
+
>
|
|
44
|
+
<div className="mt-0.5">
|
|
45
|
+
<StatusIcon status={todo.status} />
|
|
46
|
+
</div>
|
|
47
|
+
<span className={todo.status === "completed" ? "line-through" : ""}>
|
|
48
|
+
{todo.content}
|
|
49
|
+
</span>
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
})}
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
55
|
+
</div>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useEffect } from "react";
|
|
2
2
|
import { createPortal } from "react-dom";
|
|
3
3
|
import Markdown from "react-markdown";
|
|
4
|
+
import remarkGfm from "remark-gfm";
|
|
4
5
|
import { Columns2, AlignLeft, FileText, Image } from "lucide-react";
|
|
5
6
|
|
|
6
7
|
interface ToolResultRendererProps {
|
|
@@ -324,7 +325,7 @@ export function ToolResultRenderer(props: ToolResultRendererProps) {
|
|
|
324
325
|
<div className="px-2.5 py-2">
|
|
325
326
|
<div className="text-[9px] text-base-content/25 uppercase tracking-wider font-semibold mb-0.5">Result</div>
|
|
326
327
|
<div className="prose prose-sm max-w-none text-[11px] [&_table]:text-[11px] [&_th]:px-2 [&_th]:py-1 [&_td]:px-2 [&_td]:py-1 [&_th]:text-base-content/60 [&_td]:text-base-content/45 [&_table]:border-base-content/10 [&_th]:border-base-content/10 [&_td]:border-base-content/10 [&_th]:bg-base-100/50">
|
|
327
|
-
<Markdown>{result}</Markdown>
|
|
328
|
+
<Markdown remarkPlugins={[remarkGfm]}>{result}</Markdown>
|
|
328
329
|
</div>
|
|
329
330
|
</div>
|
|
330
331
|
);
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Mic } from "lucide-react";
|
|
2
|
+
|
|
3
|
+
interface VoiceRecorderProps {
|
|
4
|
+
isRecording: boolean;
|
|
5
|
+
isSupported: boolean;
|
|
6
|
+
isSpeaking: boolean;
|
|
7
|
+
elapsed: number;
|
|
8
|
+
interimTranscript: string;
|
|
9
|
+
onStart: () => void;
|
|
10
|
+
onStop: () => void;
|
|
11
|
+
onCancel: () => void;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function formatTime(seconds: number): string {
|
|
15
|
+
var m = Math.floor(seconds / 60);
|
|
16
|
+
var s = seconds % 60;
|
|
17
|
+
return m + ":" + (s < 10 ? "0" : "") + s;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function VoiceRecorder(props: VoiceRecorderProps) {
|
|
21
|
+
if (!props.isRecording) {
|
|
22
|
+
return (
|
|
23
|
+
<button
|
|
24
|
+
aria-label={props.isSupported ? "Start voice input" : "Voice input not supported"}
|
|
25
|
+
disabled={!props.isSupported}
|
|
26
|
+
onClick={props.onStart}
|
|
27
|
+
className={
|
|
28
|
+
"w-7 h-7 rounded-md flex items-center justify-center transition-colors flex-shrink-0 " +
|
|
29
|
+
(props.isSupported
|
|
30
|
+
? "text-base-content/30 hover:text-base-content/50 border border-base-content/10 hover:border-base-content/20"
|
|
31
|
+
: "text-base-content/15 cursor-not-allowed")
|
|
32
|
+
}
|
|
33
|
+
title={props.isSupported ? "Voice input" : "Voice input not supported in this browser"}
|
|
34
|
+
>
|
|
35
|
+
<Mic size={13} />
|
|
36
|
+
</button>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<div className="flex items-center gap-2 flex-1 min-w-0">
|
|
42
|
+
<button
|
|
43
|
+
aria-label="Stop recording"
|
|
44
|
+
onClick={props.onStop}
|
|
45
|
+
className="w-7 h-7 rounded-md flex items-center justify-center bg-error/15 text-error border border-error/30 animate-pulse flex-shrink-0"
|
|
46
|
+
>
|
|
47
|
+
<Mic size={13} />
|
|
48
|
+
</button>
|
|
49
|
+
|
|
50
|
+
<div className="flex items-center gap-[2px] h-5" aria-hidden="true">
|
|
51
|
+
{Array.from({ length: 8 }).map(function (_, i) {
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
key={i}
|
|
55
|
+
className={
|
|
56
|
+
"w-[2px] rounded-sm bg-error transition-all duration-300 " +
|
|
57
|
+
(props.isSpeaking ? "animate-waveform" : "")
|
|
58
|
+
}
|
|
59
|
+
style={{
|
|
60
|
+
height: props.isSpeaking ? undefined : "4px",
|
|
61
|
+
opacity: props.isSpeaking ? undefined : 0.3,
|
|
62
|
+
animationDelay: (i * 0.1) + "s",
|
|
63
|
+
}}
|
|
64
|
+
/>
|
|
65
|
+
);
|
|
66
|
+
})}
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<span className="text-[12px] text-error font-mono flex-shrink-0">{formatTime(props.elapsed)}</span>
|
|
70
|
+
|
|
71
|
+
{props.interimTranscript && (
|
|
72
|
+
<span className="text-[11px] text-base-content/30 truncate flex-1 min-w-0">
|
|
73
|
+
{props.interimTranscript.slice(-60)}
|
|
74
|
+
</span>
|
|
75
|
+
)}
|
|
76
|
+
|
|
77
|
+
<button
|
|
78
|
+
onClick={props.onCancel}
|
|
79
|
+
className="text-[11px] text-base-content/40 hover:text-base-content/60 border border-base-content/10 rounded-md px-2 py-0.5 flex-shrink-0"
|
|
80
|
+
>
|
|
81
|
+
Cancel
|
|
82
|
+
</button>
|
|
83
|
+
</div>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useState, useEffect } from "react";
|
|
2
|
-
import { Plus, Trash2, Pencil, X, Loader2, Brain } from "lucide-react";
|
|
2
|
+
import { Plus, Trash2, Pencil, X, Loader2, Brain, ExternalLink } from "lucide-react";
|
|
3
3
|
import Markdown from "react-markdown";
|
|
4
|
+
import remarkGfm from "remark-gfm";
|
|
4
5
|
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
5
6
|
import type { ServerMessage } from "@lattice/shared";
|
|
6
7
|
|
|
@@ -167,7 +168,7 @@ function MemoryViewModal({
|
|
|
167
168
|
|
|
168
169
|
<div className="px-5 py-4">
|
|
169
170
|
<div className="prose prose-sm max-w-none prose-headings:text-base-content prose-headings:font-mono prose-p:text-base-content/70 prose-strong:text-base-content prose-code:text-base-content/60 prose-code:bg-base-100/50 prose-code:px-1 prose-code:py-0.5 prose-code:rounded prose-code:text-[11px] prose-pre:bg-base-100 prose-pre:text-base-content/70 prose-pre:text-[11px] prose-a:text-primary prose-li:text-base-content/70 prose-li:marker:text-base-content/30 [&>*:first-child]:mt-0 [&>*:last-child]:mb-0">
|
|
170
|
-
<Markdown>{parsed.body}</Markdown>
|
|
171
|
+
<Markdown remarkPlugins={[remarkGfm]}>{parsed.body}</Markdown>
|
|
171
172
|
</div>
|
|
172
173
|
</div>
|
|
173
174
|
</div>
|
|
@@ -404,6 +405,15 @@ export function ProjectMemory({ projectSlug }: ProjectMemoryProps) {
|
|
|
404
405
|
|
|
405
406
|
return (
|
|
406
407
|
<div className="py-2 space-y-6">
|
|
408
|
+
<a
|
|
409
|
+
href="https://docs.anthropic.com/en/docs/claude-code/memory"
|
|
410
|
+
target="_blank"
|
|
411
|
+
rel="noopener noreferrer"
|
|
412
|
+
className="text-[11px] text-base-content/30 hover:text-primary/70 flex items-center gap-1 transition-colors"
|
|
413
|
+
>
|
|
414
|
+
<ExternalLink size={11} />
|
|
415
|
+
Claude Code docs
|
|
416
|
+
</a>
|
|
407
417
|
<div className="flex items-center justify-end">
|
|
408
418
|
<button
|
|
409
419
|
onClick={function () { setEditState({ memory: null, content: "" }); }}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
|
|
3
|
+
interface ProjectNotificationsProps {
|
|
4
|
+
projectSlug?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function loadMuted(slug: string): boolean {
|
|
8
|
+
return localStorage.getItem("lattice-mute-" + slug) === "1";
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function saveMuted(slug: string, muted: boolean): void {
|
|
12
|
+
localStorage.setItem("lattice-mute-" + slug, muted ? "1" : "0");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function ProjectNotifications({ projectSlug }: ProjectNotificationsProps) {
|
|
16
|
+
var slug = projectSlug ?? "";
|
|
17
|
+
var [muted, setMuted] = useState(function () { return loadMuted(slug); });
|
|
18
|
+
|
|
19
|
+
function toggle() {
|
|
20
|
+
var next = !muted;
|
|
21
|
+
setMuted(next);
|
|
22
|
+
saveMuted(slug, next);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return (
|
|
26
|
+
<div className="py-2 space-y-6">
|
|
27
|
+
<div>
|
|
28
|
+
<h2 className="text-[12px] font-semibold text-base-content/40 mb-3">
|
|
29
|
+
Project Notifications
|
|
30
|
+
</h2>
|
|
31
|
+
<div className="flex items-center justify-between py-2 px-3 bg-base-300 border border-base-content/15 rounded-xl">
|
|
32
|
+
<div>
|
|
33
|
+
<div className="text-[13px] text-base-content">Mute all notifications</div>
|
|
34
|
+
<div className="text-[11px] text-base-content/30 mt-0.5">
|
|
35
|
+
Suppress all browser notifications from this project
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
<input
|
|
39
|
+
type="checkbox"
|
|
40
|
+
className="toggle toggle-sm toggle-primary"
|
|
41
|
+
checked={muted}
|
|
42
|
+
onChange={toggle}
|
|
43
|
+
/>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect } from "react";
|
|
2
|
-
import { X, Plus, ChevronDown, ChevronRight } from "lucide-react";
|
|
2
|
+
import { X, Plus, ChevronDown, ChevronRight, ExternalLink } from "lucide-react";
|
|
3
3
|
import { SaveFooter } from "../ui/SaveFooter";
|
|
4
4
|
import { useSaveState } from "../../hooks/useSaveState";
|
|
5
5
|
import type { ProjectSettings } from "@lattice/shared";
|
|
@@ -116,6 +116,15 @@ export function ProjectRules({
|
|
|
116
116
|
|
|
117
117
|
return (
|
|
118
118
|
<div className="py-2">
|
|
119
|
+
<a
|
|
120
|
+
href="https://docs.anthropic.com/en/docs/claude-code/memory"
|
|
121
|
+
target="_blank"
|
|
122
|
+
rel="noopener noreferrer"
|
|
123
|
+
className="text-[11px] text-base-content/30 hover:text-primary/70 flex items-center gap-1 mb-4 transition-colors"
|
|
124
|
+
>
|
|
125
|
+
<ExternalLink size={11} />
|
|
126
|
+
Claude Code docs
|
|
127
|
+
</a>
|
|
119
128
|
<div className="mb-6">
|
|
120
129
|
<h2 className="text-[12px] font-semibold text-base-content/40 mb-3">
|
|
121
130
|
Global Rules
|
|
@@ -11,9 +11,11 @@ import { ProjectSkills } from "./ProjectSkills";
|
|
|
11
11
|
import { ProjectRules } from "./ProjectRules";
|
|
12
12
|
import { ProjectMcp } from "./ProjectMcp";
|
|
13
13
|
import { ProjectMemory } from "./ProjectMemory";
|
|
14
|
+
import { ProjectNotifications } from "./ProjectNotifications";
|
|
14
15
|
|
|
15
16
|
var SECTION_CONFIG: Record<string, { title: string }> = {
|
|
16
17
|
general: { title: "General" },
|
|
18
|
+
notifications: { title: "Notifications" },
|
|
17
19
|
claude: { title: "Claude" },
|
|
18
20
|
environment: { title: "Environment" },
|
|
19
21
|
mcp: { title: "MCP Servers" },
|
|
@@ -61,6 +63,10 @@ function renderSection(
|
|
|
61
63
|
return <ProjectMemory projectSlug={projectSlug} />;
|
|
62
64
|
}
|
|
63
65
|
|
|
66
|
+
if (section === "notifications") {
|
|
67
|
+
return <ProjectNotifications projectSlug={projectSlug} />;
|
|
68
|
+
}
|
|
69
|
+
|
|
64
70
|
return (
|
|
65
71
|
<div className="py-2 text-[13px] text-base-content/40">
|
|
66
72
|
{section} section coming soon.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useState, useEffect } from "react";
|
|
2
|
+
import { ExternalLink } from "lucide-react";
|
|
2
3
|
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
3
4
|
import { useSaveState } from "../../hooks/useSaveState";
|
|
4
5
|
import { SaveFooter } from "../ui/SaveFooter";
|
|
@@ -82,6 +83,15 @@ export function ClaudeSettings() {
|
|
|
82
83
|
|
|
83
84
|
return (
|
|
84
85
|
<div className="py-2">
|
|
86
|
+
<a
|
|
87
|
+
href="https://docs.anthropic.com/en/docs/claude-code/memory"
|
|
88
|
+
target="_blank"
|
|
89
|
+
rel="noopener noreferrer"
|
|
90
|
+
className="text-[11px] text-base-content/30 hover:text-primary/70 flex items-center gap-1 mb-4 transition-colors"
|
|
91
|
+
>
|
|
92
|
+
<ExternalLink size={11} />
|
|
93
|
+
Claude Code docs
|
|
94
|
+
</a>
|
|
85
95
|
<div className="mb-5">
|
|
86
96
|
<label htmlFor="claude-default-model" className="block text-[12px] font-semibold text-base-content/40 mb-2">Default Model</label>
|
|
87
97
|
<select
|