@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
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { useEffect, useRef, useCallback, useState } from "react";
|
|
2
|
-
import { Terminal, Info, ArrowDown, Pencil, Copy, Check, Menu, AlertTriangle } from "lucide-react";
|
|
2
|
+
import { Terminal, Info, ArrowDown, Pencil, Copy, Check, Menu, AlertTriangle, Zap, Square, X } from "lucide-react";
|
|
3
3
|
import { LatticeLogomark } from "../ui/LatticeLogomark";
|
|
4
4
|
import { useVirtualizer } from "@tanstack/react-virtual";
|
|
5
5
|
import { useSession } from "../../hooks/useSession";
|
|
6
6
|
import { useProjects } from "../../hooks/useProjects";
|
|
7
7
|
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
8
|
-
import { setSessionTitle } from "../../stores/session";
|
|
8
|
+
import { setSessionTitle, setIsProcessing, setCurrentStatus, setWasInterrupted } from "../../stores/session";
|
|
9
|
+
import { openSettings, openProjectSettings } from "../../stores/sidebar";
|
|
10
|
+
import { openTab } from "../../stores/workspace";
|
|
11
|
+
import { builtinCommands } from "../../commands";
|
|
9
12
|
import { Message } from "./Message";
|
|
10
13
|
import { ToolGroup } from "./ToolGroup";
|
|
11
14
|
import { ChatInput } from "./ChatInput";
|
|
@@ -13,12 +16,16 @@ import { ModelSelector } from "./ModelSelector";
|
|
|
13
16
|
import { PermissionModeSelector } from "./PermissionModeSelector";
|
|
14
17
|
import { StatusBar } from "./StatusBar";
|
|
15
18
|
import { useSidebar } from "../../hooks/useSidebar";
|
|
19
|
+
import { useOnline } from "../../hooks/useOnline";
|
|
20
|
+
import { useSpinnerVerb } from "../../hooks/useSpinnerVerb";
|
|
16
21
|
|
|
17
22
|
export function ChatView() {
|
|
18
|
-
var { messages, isProcessing, sendMessage, activeSessionId, activeSessionTitle, currentStatus, contextUsage, contextBreakdown, lastResponseCost, lastResponseDuration, historyLoading, wasInterrupted } = useSession();
|
|
23
|
+
var { messages, isProcessing, sendMessage, activeSessionId, activeSessionTitle, currentStatus, contextUsage, contextBreakdown, lastResponseCost, lastResponseDuration, historyLoading, wasInterrupted, promptSuggestion, failedInput, clearFailedInput, messageQueue, enqueueMessage, removeQueuedMessage, updateQueuedMessage, isBusy, isPlanMode } = useSession();
|
|
19
24
|
var { activeProject } = useProjects();
|
|
20
25
|
var { toggleDrawer } = useSidebar();
|
|
26
|
+
var online = useOnline();
|
|
21
27
|
var ws = useWebSocket();
|
|
28
|
+
var spinnerVerb = useSpinnerVerb(isProcessing);
|
|
22
29
|
var scrollParentRef = useRef<HTMLDivElement>(null);
|
|
23
30
|
var prevLengthRef = useRef<number>(0);
|
|
24
31
|
var isLiveChatRef = useRef<boolean>(false);
|
|
@@ -28,6 +35,8 @@ export function ChatView() {
|
|
|
28
35
|
var [selectedModel, setSelectedModel] = useState<string>("default");
|
|
29
36
|
var [selectedEffort, setSelectedEffort] = useState<string>("medium");
|
|
30
37
|
var [showInfo, setShowInfo] = useState<boolean>(false);
|
|
38
|
+
var [confirmStopExternal, setConfirmStopExternal] = useState<boolean>(false);
|
|
39
|
+
var [prefillText, setPrefillText] = useState<string | null>(null);
|
|
31
40
|
var [copiedField, setCopiedField] = useState<string | null>(null);
|
|
32
41
|
var [isRenaming, setIsRenaming] = useState<boolean>(false);
|
|
33
42
|
var [renameValue, setRenameValue] = useState<string>("");
|
|
@@ -244,6 +253,106 @@ export function ChatView() {
|
|
|
244
253
|
? "claude --resume " + activeSessionId
|
|
245
254
|
: "";
|
|
246
255
|
|
|
256
|
+
function handleClientCommand(name: string, args: string): boolean {
|
|
257
|
+
switch (name) {
|
|
258
|
+
case "clear":
|
|
259
|
+
case "reset":
|
|
260
|
+
case "new":
|
|
261
|
+
if (activeProject?.slug) {
|
|
262
|
+
ws.send({ type: "session:create", projectSlug: activeProject.slug });
|
|
263
|
+
}
|
|
264
|
+
return true;
|
|
265
|
+
case "copy": {
|
|
266
|
+
var lastAssistant = [...messages].reverse().find(function (m) { return m.type === "assistant"; });
|
|
267
|
+
if (lastAssistant?.text) {
|
|
268
|
+
navigator.clipboard.writeText(lastAssistant.text);
|
|
269
|
+
}
|
|
270
|
+
return true;
|
|
271
|
+
}
|
|
272
|
+
case "export": {
|
|
273
|
+
var lines = messages.map(function (m) {
|
|
274
|
+
var role = m.type === "user" ? "User" : m.type === "assistant" ? "Assistant" : m.type;
|
|
275
|
+
return role + ": " + (m.text || m.content || "");
|
|
276
|
+
});
|
|
277
|
+
var blob = new Blob([lines.join("\n\n")], { type: "text/plain" });
|
|
278
|
+
var url = URL.createObjectURL(blob);
|
|
279
|
+
var a = document.createElement("a");
|
|
280
|
+
a.href = url;
|
|
281
|
+
a.download = (activeSessionTitle || "conversation") + ".txt";
|
|
282
|
+
a.click();
|
|
283
|
+
URL.revokeObjectURL(url);
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
case "rename": {
|
|
287
|
+
if (args && activeSessionId) {
|
|
288
|
+
ws.send({ type: "session:rename", sessionId: activeSessionId, title: args });
|
|
289
|
+
setSessionTitle(args);
|
|
290
|
+
} else {
|
|
291
|
+
handleRenameStart();
|
|
292
|
+
}
|
|
293
|
+
return true;
|
|
294
|
+
}
|
|
295
|
+
case "theme":
|
|
296
|
+
openSettings("appearance");
|
|
297
|
+
return true;
|
|
298
|
+
case "config":
|
|
299
|
+
case "settings":
|
|
300
|
+
openSettings("appearance");
|
|
301
|
+
return true;
|
|
302
|
+
case "permissions":
|
|
303
|
+
case "allowed-tools":
|
|
304
|
+
if (activeProject?.slug) openProjectSettings("permissions");
|
|
305
|
+
return true;
|
|
306
|
+
case "memory":
|
|
307
|
+
if (activeProject?.slug) openProjectSettings("memory");
|
|
308
|
+
return true;
|
|
309
|
+
case "skills":
|
|
310
|
+
if (activeProject?.slug) openProjectSettings("skills");
|
|
311
|
+
return true;
|
|
312
|
+
case "plan":
|
|
313
|
+
ws.send({ type: "chat:set_permission_mode", mode: "plan" });
|
|
314
|
+
return true;
|
|
315
|
+
case "cost":
|
|
316
|
+
case "context":
|
|
317
|
+
setShowInfo(true);
|
|
318
|
+
return true;
|
|
319
|
+
default:
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function handleCancel() {
|
|
325
|
+
ws.send({ type: "chat:cancel" });
|
|
326
|
+
setIsProcessing(false);
|
|
327
|
+
setCurrentStatus(null);
|
|
328
|
+
setWasInterrupted(true);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function handleSend(text: string, attachmentIds: string[]) {
|
|
332
|
+
if (text.startsWith("/")) {
|
|
333
|
+
var parts = text.split(/\s+/);
|
|
334
|
+
var cmdName = parts[0].slice(1).toLowerCase();
|
|
335
|
+
var cmdArgs = parts.slice(1).join(" ");
|
|
336
|
+
|
|
337
|
+
var isBuiltin = false;
|
|
338
|
+
for (var i = 0; i < builtinCommands.length; i++) {
|
|
339
|
+
var cmd = builtinCommands[i];
|
|
340
|
+
if (cmd.name === cmdName || (cmd.aliases && cmd.aliases.indexOf(cmdName) !== -1)) {
|
|
341
|
+
isBuiltin = true;
|
|
342
|
+
break;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (isBuiltin && handleClientCommand(cmdName, cmdArgs)) return;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (isProcessing) {
|
|
350
|
+
enqueueMessage(text);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
sendMessage(text, attachmentIds, selectedModel, selectedEffort);
|
|
354
|
+
}
|
|
355
|
+
|
|
247
356
|
var virtualItems = virtualizer.getVirtualItems();
|
|
248
357
|
|
|
249
358
|
return (
|
|
@@ -324,9 +433,8 @@ export function ChatView() {
|
|
|
324
433
|
)}
|
|
325
434
|
<button
|
|
326
435
|
aria-label="Open terminal"
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
className="btn btn-ghost btn-sm btn-square text-base-content/30 opacity-40 cursor-not-allowed"
|
|
436
|
+
onClick={function () { openTab("terminal"); }}
|
|
437
|
+
className="btn btn-ghost btn-sm btn-square text-base-content/50 hover:text-base-content/70"
|
|
330
438
|
>
|
|
331
439
|
<Terminal size={15} />
|
|
332
440
|
</button>
|
|
@@ -505,9 +613,16 @@ export function ChatView() {
|
|
|
505
613
|
)}
|
|
506
614
|
</div>
|
|
507
615
|
|
|
616
|
+
{isPlanMode && (
|
|
617
|
+
<div className="flex items-center gap-2 px-4 py-1.5 bg-primary/8 border-b border-primary/15">
|
|
618
|
+
<div className="w-2 h-2 rounded-full bg-primary animate-pulse" />
|
|
619
|
+
<span className="text-[11px] font-mono font-medium text-primary/60 uppercase tracking-wider">Plan Mode</span>
|
|
620
|
+
</div>
|
|
621
|
+
)}
|
|
622
|
+
|
|
508
623
|
<div
|
|
509
624
|
ref={scrollParentRef}
|
|
510
|
-
className="flex-1 overflow-y-auto min-h-0 bg-lattice-grid"
|
|
625
|
+
className="flex-1 overflow-y-auto overflow-x-hidden min-h-0 bg-lattice-grid"
|
|
511
626
|
style={{ WebkitOverflowScrolling: "touch" }}
|
|
512
627
|
>
|
|
513
628
|
{messages.length === 0 && historyLoading ? (
|
|
@@ -702,17 +817,134 @@ export function ChatView() {
|
|
|
702
817
|
|
|
703
818
|
<StatusBar status={currentStatus} />
|
|
704
819
|
|
|
705
|
-
{
|
|
820
|
+
{isProcessing && (
|
|
821
|
+
<div className="flex-shrink-0 flex items-center justify-center gap-4 my-4 pointer-events-none relative z-10">
|
|
822
|
+
<div className="flex items-center gap-4 pointer-events-auto bg-base-200/90 backdrop-blur-sm border border-base-content/10 rounded-full px-5 py-2 shadow-lg">
|
|
823
|
+
<span className="text-[14px] text-base-content/40 font-mono animate-pulse">{spinnerVerb}...</span>
|
|
824
|
+
<button
|
|
825
|
+
onClick={handleCancel}
|
|
826
|
+
className="btn btn-ghost btn-sm text-error/70 hover:text-error gap-1.5"
|
|
827
|
+
>
|
|
828
|
+
<Square size={12} className="fill-current" />
|
|
829
|
+
Stop
|
|
830
|
+
</button>
|
|
831
|
+
</div>
|
|
832
|
+
</div>
|
|
833
|
+
)}
|
|
834
|
+
|
|
835
|
+
{messageQueue.length > 0 && (
|
|
836
|
+
<div className="flex-shrink-0 px-2 sm:px-4 py-2 space-y-1.5">
|
|
837
|
+
<div className="text-[10px] font-semibold uppercase tracking-wider text-base-content/30">Queued</div>
|
|
838
|
+
{messageQueue.map(function (msg, i) {
|
|
839
|
+
return (
|
|
840
|
+
<div key={i} className="flex items-center gap-2 px-3 py-1.5 rounded-lg bg-base-300/50 border border-base-content/10">
|
|
841
|
+
<input
|
|
842
|
+
type="text"
|
|
843
|
+
value={msg}
|
|
844
|
+
onChange={function (e) { updateQueuedMessage(i, e.target.value); }}
|
|
845
|
+
className="flex-1 bg-transparent text-[12px] text-base-content outline-none"
|
|
846
|
+
/>
|
|
847
|
+
<button
|
|
848
|
+
onClick={function () { removeQueuedMessage(i); }}
|
|
849
|
+
className="text-base-content/30 hover:text-base-content/60"
|
|
850
|
+
>
|
|
851
|
+
<X size={12} />
|
|
852
|
+
</button>
|
|
853
|
+
</div>
|
|
854
|
+
);
|
|
855
|
+
})}
|
|
856
|
+
</div>
|
|
857
|
+
)}
|
|
858
|
+
|
|
859
|
+
{isBusy && !isProcessing && (
|
|
860
|
+
<div className="flex items-center gap-2 px-3 sm:px-5 py-2 bg-info/10 border-t border-info/20">
|
|
861
|
+
<Terminal size={13} className="text-info flex-shrink-0" />
|
|
862
|
+
<span className="text-[12px] text-info flex-1">This session is being used by another client — input is disabled</span>
|
|
863
|
+
<button
|
|
864
|
+
onClick={function () { setConfirmStopExternal(true); }}
|
|
865
|
+
className="btn btn-ghost btn-xs text-error/70 hover:text-error gap-1 flex-shrink-0"
|
|
866
|
+
>
|
|
867
|
+
<Square size={10} className="fill-current" />
|
|
868
|
+
End Process
|
|
869
|
+
</button>
|
|
870
|
+
</div>
|
|
871
|
+
)}
|
|
872
|
+
|
|
873
|
+
{confirmStopExternal && (
|
|
874
|
+
<div className="fixed inset-0 z-[9999] flex items-center justify-center">
|
|
875
|
+
<div className="absolute inset-0 bg-black/50" onClick={function () { setConfirmStopExternal(false); }} />
|
|
876
|
+
<div className="relative bg-base-200 border border-base-content/15 rounded-2xl shadow-2xl w-full max-w-sm mx-4 overflow-hidden">
|
|
877
|
+
<div className="px-5 py-4 border-b border-base-content/15">
|
|
878
|
+
<h2 className="text-[15px] font-mono font-bold text-base-content">End External Process</h2>
|
|
879
|
+
</div>
|
|
880
|
+
<div className="px-5 py-4">
|
|
881
|
+
<p className="text-[13px] text-base-content/60 leading-relaxed">
|
|
882
|
+
This will send a graceful stop signal (SIGINT) to the Claude Code CLI process controlling this session. The process will finish its current operation and exit.
|
|
883
|
+
</p>
|
|
884
|
+
<p className="text-[13px] text-warning mt-2 leading-relaxed">
|
|
885
|
+
Any in-progress work may be interrupted.
|
|
886
|
+
</p>
|
|
887
|
+
</div>
|
|
888
|
+
<div className="px-5 py-3 border-t border-base-content/15 flex justify-end gap-2">
|
|
889
|
+
<button
|
|
890
|
+
onClick={function () { setConfirmStopExternal(false); }}
|
|
891
|
+
className="btn btn-ghost btn-sm text-[12px]"
|
|
892
|
+
>
|
|
893
|
+
Cancel
|
|
894
|
+
</button>
|
|
895
|
+
<button
|
|
896
|
+
onClick={function () {
|
|
897
|
+
if (activeSessionId) {
|
|
898
|
+
ws.send({ type: "session:stop_external", sessionId: activeSessionId } as any);
|
|
899
|
+
}
|
|
900
|
+
setConfirmStopExternal(false);
|
|
901
|
+
}}
|
|
902
|
+
className="btn btn-error btn-sm text-[12px]"
|
|
903
|
+
>
|
|
904
|
+
End Process
|
|
905
|
+
</button>
|
|
906
|
+
</div>
|
|
907
|
+
</div>
|
|
908
|
+
</div>
|
|
909
|
+
)}
|
|
910
|
+
|
|
911
|
+
{wasInterrupted && !isProcessing && !isBusy && (
|
|
706
912
|
<div className="flex items-center gap-2 px-3 sm:px-5 py-2 bg-warning/10 border-t border-warning/20">
|
|
707
913
|
<AlertTriangle size={13} className="text-warning flex-shrink-0" />
|
|
708
914
|
<span className="text-[12px] text-warning">Session was interrupted — send a message to continue</span>
|
|
709
915
|
</div>
|
|
710
916
|
)}
|
|
711
917
|
|
|
918
|
+
{promptSuggestion && !isProcessing && !isBusy && (
|
|
919
|
+
<div className="flex-shrink-0 px-2 sm:px-4 py-2">
|
|
920
|
+
<div className="flex items-center gap-1.5 max-w-full">
|
|
921
|
+
<button
|
|
922
|
+
onClick={function () { if (promptSuggestion) handleSend(promptSuggestion, []); }}
|
|
923
|
+
className="flex items-center gap-2 px-3 py-2 rounded-lg bg-primary/10 border border-primary/20 text-[12px] text-primary/80 hover:bg-primary/15 hover:text-primary transition-colors min-w-0"
|
|
924
|
+
>
|
|
925
|
+
<Zap size={12} className="flex-shrink-0" />
|
|
926
|
+
<span className="truncate">{promptSuggestion}</span>
|
|
927
|
+
</button>
|
|
928
|
+
<button
|
|
929
|
+
onClick={function () { if (promptSuggestion) setPrefillText(promptSuggestion); }}
|
|
930
|
+
aria-label="Edit suggestion"
|
|
931
|
+
className="btn btn-ghost btn-xs btn-square text-primary/40 hover:text-primary/80 flex-shrink-0"
|
|
932
|
+
>
|
|
933
|
+
<Pencil size={12} />
|
|
934
|
+
</button>
|
|
935
|
+
</div>
|
|
936
|
+
</div>
|
|
937
|
+
)}
|
|
938
|
+
|
|
712
939
|
<div className="flex-shrink-0 border-t border-base-300 bg-base-200 px-2 sm:px-4 pb-3 pt-2">
|
|
713
940
|
<ChatInput
|
|
714
|
-
onSend={
|
|
715
|
-
disabled={
|
|
941
|
+
onSend={handleSend}
|
|
942
|
+
disabled={!activeSessionId || !online || isBusy}
|
|
943
|
+
disabledPlaceholder={isBusy ? "Session in use by another client..." : undefined}
|
|
944
|
+
failedInput={failedInput}
|
|
945
|
+
onFailedInputConsumed={clearFailedInput}
|
|
946
|
+
prefillText={prefillText}
|
|
947
|
+
onPrefillConsumed={function () { setPrefillText(null); }}
|
|
716
948
|
toolbarContent={
|
|
717
949
|
<>
|
|
718
950
|
<PermissionModeSelector />
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { useMemo } from "react";
|
|
2
|
+
import type { SkillInfo } from "@lattice/shared";
|
|
3
|
+
import { builtinCommands, type SlashCommand } from "../../commands";
|
|
4
|
+
|
|
5
|
+
interface PaletteItem {
|
|
6
|
+
name: string;
|
|
7
|
+
description: string;
|
|
8
|
+
args?: string;
|
|
9
|
+
category: "command" | "skill";
|
|
10
|
+
handler: "client" | "passthrough";
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface CommandPaletteProps {
|
|
14
|
+
query: string;
|
|
15
|
+
skills: SkillInfo[];
|
|
16
|
+
selectedIndex: number;
|
|
17
|
+
onSelect: (item: PaletteItem) => void;
|
|
18
|
+
onHover: (index: number) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function matchesQuery(q: string, cmd: SlashCommand): boolean {
|
|
22
|
+
if (cmd.name.toLowerCase().includes(q)) return true;
|
|
23
|
+
if (cmd.description.toLowerCase().includes(q)) return true;
|
|
24
|
+
if (cmd.aliases) {
|
|
25
|
+
for (var i = 0; i < cmd.aliases.length; i++) {
|
|
26
|
+
if (cmd.aliases[i].toLowerCase().includes(q)) return true;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getFilteredItems(query: string, skills: SkillInfo[]): PaletteItem[] {
|
|
33
|
+
var q = query.toLowerCase();
|
|
34
|
+
var commands: PaletteItem[] = [];
|
|
35
|
+
var skillItems: PaletteItem[] = [];
|
|
36
|
+
|
|
37
|
+
for (var i = 0; i < builtinCommands.length; i++) {
|
|
38
|
+
var cmd = builtinCommands[i];
|
|
39
|
+
if (matchesQuery(q, cmd)) {
|
|
40
|
+
commands.push({
|
|
41
|
+
name: cmd.name,
|
|
42
|
+
description: cmd.description,
|
|
43
|
+
args: cmd.args,
|
|
44
|
+
category: "command",
|
|
45
|
+
handler: cmd.handler,
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
for (var j = 0; j < skills.length; j++) {
|
|
51
|
+
var skill = skills[j];
|
|
52
|
+
if (skill.name.toLowerCase().includes(q) || skill.description.toLowerCase().includes(q)) {
|
|
53
|
+
skillItems.push({
|
|
54
|
+
name: skill.name,
|
|
55
|
+
description: skill.description,
|
|
56
|
+
category: "skill",
|
|
57
|
+
handler: "passthrough",
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return commands.concat(skillItems);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function getItemCount(query: string, skills: SkillInfo[]): number {
|
|
66
|
+
return getFilteredItems(query, skills).length;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function CommandPalette(props: CommandPaletteProps) {
|
|
70
|
+
var items = useMemo(function () {
|
|
71
|
+
return getFilteredItems(props.query, props.skills);
|
|
72
|
+
}, [props.query, props.skills]);
|
|
73
|
+
|
|
74
|
+
if (items.length === 0) {
|
|
75
|
+
return (
|
|
76
|
+
<div
|
|
77
|
+
role="listbox"
|
|
78
|
+
aria-label="Slash commands"
|
|
79
|
+
className="absolute left-0 right-0 bottom-[calc(100%+6px)] rounded-lg border border-base-content/15 bg-base-300 shadow-lg z-50"
|
|
80
|
+
>
|
|
81
|
+
<div className="px-3.5 py-3 text-[12px] text-base-content/40 text-center font-mono">
|
|
82
|
+
No matching commands
|
|
83
|
+
</div>
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
var commandItems: PaletteItem[] = [];
|
|
89
|
+
var skillItems: PaletteItem[] = [];
|
|
90
|
+
for (var i = 0; i < items.length; i++) {
|
|
91
|
+
if (items[i].category === "command") {
|
|
92
|
+
commandItems.push(items[i]);
|
|
93
|
+
} else {
|
|
94
|
+
skillItems.push(items[i]);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
var globalIndex = 0;
|
|
99
|
+
|
|
100
|
+
function renderItem(item: PaletteItem, idx: number) {
|
|
101
|
+
var currentIndex = idx;
|
|
102
|
+
return (
|
|
103
|
+
<button
|
|
104
|
+
key={item.name}
|
|
105
|
+
data-active={currentIndex === props.selectedIndex}
|
|
106
|
+
onMouseDown={function (e) {
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
props.onSelect(item);
|
|
109
|
+
}}
|
|
110
|
+
onMouseEnter={function () { props.onHover(currentIndex); }}
|
|
111
|
+
className={
|
|
112
|
+
"flex w-full items-center gap-3 px-3.5 py-2.5 text-left transition-colors " +
|
|
113
|
+
(currentIndex === props.selectedIndex ? "bg-primary/10" : "hover:bg-base-content/5")
|
|
114
|
+
}
|
|
115
|
+
>
|
|
116
|
+
<span className="font-mono text-[12px] text-primary/90 whitespace-nowrap flex-shrink-0">
|
|
117
|
+
/{item.name}
|
|
118
|
+
{item.args ? <span className="text-base-content/25 ml-1">{item.args}</span> : null}
|
|
119
|
+
</span>
|
|
120
|
+
<span className="text-[11px] text-base-content/40 truncate min-w-0">
|
|
121
|
+
{item.description}
|
|
122
|
+
</span>
|
|
123
|
+
</button>
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
var elements: React.ReactNode[] = [];
|
|
128
|
+
|
|
129
|
+
if (commandItems.length > 0) {
|
|
130
|
+
elements.push(
|
|
131
|
+
<div key="commands-header" className="px-3.5 py-1.5 text-[10px] uppercase tracking-widest text-base-content/30 font-mono font-bold">
|
|
132
|
+
Commands
|
|
133
|
+
</div>
|
|
134
|
+
);
|
|
135
|
+
for (var ci = 0; ci < commandItems.length; ci++) {
|
|
136
|
+
elements.push(renderItem(commandItems[ci], globalIndex));
|
|
137
|
+
globalIndex++;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (skillItems.length > 0) {
|
|
142
|
+
elements.push(
|
|
143
|
+
<div key="skills-header" className="px-3.5 py-1.5 text-[10px] uppercase tracking-widest text-base-content/30 font-mono font-bold">
|
|
144
|
+
Skills
|
|
145
|
+
</div>
|
|
146
|
+
);
|
|
147
|
+
for (var si = 0; si < skillItems.length; si++) {
|
|
148
|
+
elements.push(renderItem(skillItems[si], globalIndex));
|
|
149
|
+
globalIndex++;
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return (
|
|
154
|
+
<div
|
|
155
|
+
role="listbox"
|
|
156
|
+
aria-label="Slash commands"
|
|
157
|
+
className="absolute left-0 right-0 bottom-[calc(100%+6px)] max-h-[320px] overflow-y-auto rounded-lg border border-base-content/15 bg-base-300 shadow-lg z-50"
|
|
158
|
+
>
|
|
159
|
+
{elements}
|
|
160
|
+
</div>
|
|
161
|
+
);
|
|
162
|
+
}
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
import { useState, useRef, useEffect } from "react";
|
|
2
2
|
import Markdown from "react-markdown";
|
|
3
|
+
import remarkGfm from "remark-gfm";
|
|
3
4
|
import { Wrench, TriangleAlert, ChevronDown, Check, X, Shield } from "lucide-react";
|
|
4
5
|
import type { HistoryMessage, ChatPermissionResponseMessage } from "@lattice/shared";
|
|
5
6
|
import { useWebSocket } from "../../hooks/useWebSocket";
|
|
6
7
|
import { ToolResultRenderer } from "./ToolResultRenderer";
|
|
7
8
|
import { formatToolSummary } from "./toolSummary";
|
|
9
|
+
import { PromptQuestion } from "./PromptQuestion";
|
|
10
|
+
import { TodoCard } from "./TodoCard";
|
|
11
|
+
|
|
12
|
+
var mdComponents = {
|
|
13
|
+
table: function (props: React.HTMLAttributes<HTMLTableElement>) {
|
|
14
|
+
return (
|
|
15
|
+
<div className="table-wrapper">
|
|
16
|
+
<table {...props} />
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
},
|
|
20
|
+
};
|
|
8
21
|
|
|
9
22
|
interface MessageProps {
|
|
10
23
|
message: HistoryMessage;
|
|
@@ -41,7 +54,7 @@ function UserMessage(props: { message: HistoryMessage }) {
|
|
|
41
54
|
<div className="chat chat-end px-5 py-1">
|
|
42
55
|
<div className="chat-bubble chat-bubble-primary text-[13px] leading-relaxed break-words max-w-[95%] sm:max-w-[85%] shadow-sm">
|
|
43
56
|
<div className="prose prose-sm max-w-none [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 prose-headings:text-primary-content prose-p:text-primary-content prose-strong:text-primary-content prose-code:text-primary-content/80 prose-pre:bg-primary/20 prose-a:text-primary-content/90 prose-a:underline">
|
|
44
|
-
<Markdown>{msg.text || ""}</Markdown>
|
|
57
|
+
<Markdown remarkPlugins={[remarkGfm]} components={mdComponents}>{msg.text || ""}</Markdown>
|
|
45
58
|
</div>
|
|
46
59
|
</div>
|
|
47
60
|
{time && (
|
|
@@ -79,7 +92,7 @@ function AssistantMessage(props: { message: HistoryMessage; responseCost?: numbe
|
|
|
79
92
|
</div>
|
|
80
93
|
<div className="chat-bubble bg-base-300/70 text-base-content text-[13px] leading-relaxed break-words max-w-[95%] sm:max-w-[85%] shadow-sm border border-base-content/5">
|
|
81
94
|
<div className="prose prose-sm max-w-none [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 prose-headings:text-base-content prose-p:text-base-content prose-strong:text-base-content prose-code:text-base-content/70 prose-code:bg-base-100/50 prose-pre:bg-base-100 prose-pre:text-base-content/70 prose-a:text-primary prose-a:underline prose-li:text-base-content">
|
|
82
|
-
<Markdown>{msg.text || ""}</Markdown>
|
|
95
|
+
<Markdown remarkPlugins={[remarkGfm]} components={mdComponents}>{msg.text || ""}</Markdown>
|
|
83
96
|
</div>
|
|
84
97
|
</div>
|
|
85
98
|
{time && (
|
|
@@ -358,5 +371,13 @@ export function Message(props: MessageProps) {
|
|
|
358
371
|
return <PermissionMessage message={msg} />;
|
|
359
372
|
}
|
|
360
373
|
|
|
374
|
+
if (msg.type === "prompt_question") {
|
|
375
|
+
return <PromptQuestion message={msg} />;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (msg.type === "todo_update") {
|
|
379
|
+
return <TodoCard message={msg} />;
|
|
380
|
+
}
|
|
381
|
+
|
|
361
382
|
return null;
|
|
362
383
|
}
|