@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.
Files changed (92) hide show
  1. package/bun.lock +705 -2
  2. package/client/index.html +1 -13
  3. package/client/package.json +6 -1
  4. package/client/src/App.tsx +2 -0
  5. package/client/src/commands.ts +36 -0
  6. package/client/src/components/chat/AttachmentChips.tsx +116 -0
  7. package/client/src/components/chat/ChatInput.tsx +250 -73
  8. package/client/src/components/chat/ChatView.tsx +242 -10
  9. package/client/src/components/chat/CommandPalette.tsx +162 -0
  10. package/client/src/components/chat/Message.tsx +23 -2
  11. package/client/src/components/chat/PromptQuestion.tsx +271 -0
  12. package/client/src/components/chat/TodoCard.tsx +57 -0
  13. package/client/src/components/chat/ToolResultRenderer.tsx +2 -1
  14. package/client/src/components/chat/VoiceRecorder.tsx +85 -0
  15. package/client/src/components/project-settings/ProjectMemory.tsx +12 -2
  16. package/client/src/components/project-settings/ProjectNotifications.tsx +48 -0
  17. package/client/src/components/project-settings/ProjectRules.tsx +10 -1
  18. package/client/src/components/project-settings/ProjectSettingsView.tsx +6 -0
  19. package/client/src/components/settings/Appearance.tsx +1 -0
  20. package/client/src/components/settings/ClaudeSettings.tsx +10 -0
  21. package/client/src/components/settings/Editor.tsx +123 -0
  22. package/client/src/components/settings/GlobalMcp.tsx +10 -1
  23. package/client/src/components/settings/GlobalMemory.tsx +19 -0
  24. package/client/src/components/settings/GlobalRules.tsx +149 -0
  25. package/client/src/components/settings/GlobalSkills.tsx +10 -0
  26. package/client/src/components/settings/Notifications.tsx +88 -0
  27. package/client/src/components/settings/SettingsView.tsx +12 -0
  28. package/client/src/components/settings/skill-shared.tsx +2 -1
  29. package/client/src/components/setup/SetupWizard.tsx +1 -1
  30. package/client/src/components/sidebar/NodeSettingsModal.tsx +23 -1
  31. package/client/src/components/sidebar/ProjectDropdown.tsx +176 -27
  32. package/client/src/components/sidebar/SettingsSidebar.tsx +11 -1
  33. package/client/src/components/sidebar/Sidebar.tsx +35 -2
  34. package/client/src/components/sidebar/UserIsland.tsx +18 -7
  35. package/client/src/components/ui/UpdatePrompt.tsx +47 -0
  36. package/client/src/components/workspace/FileBrowser.tsx +174 -0
  37. package/client/src/components/workspace/FileTree.tsx +129 -0
  38. package/client/src/components/workspace/FileViewer.tsx +211 -0
  39. package/client/src/components/workspace/NoteCard.tsx +119 -0
  40. package/client/src/components/workspace/NotesView.tsx +102 -0
  41. package/client/src/components/workspace/ScheduledTasksView.tsx +117 -0
  42. package/client/src/components/workspace/SplitPane.tsx +81 -0
  43. package/client/src/components/workspace/TabBar.tsx +185 -0
  44. package/client/src/components/workspace/TaskCard.tsx +158 -0
  45. package/client/src/components/workspace/TaskEditModal.tsx +114 -0
  46. package/client/src/components/{panels/Terminal.tsx → workspace/TerminalInstance.tsx} +50 -7
  47. package/client/src/components/workspace/TerminalView.tsx +110 -0
  48. package/client/src/components/workspace/WorkspaceView.tsx +116 -0
  49. package/client/src/hooks/useAttachments.ts +280 -0
  50. package/client/src/hooks/useEditorConfig.ts +28 -0
  51. package/client/src/hooks/useIdleDetection.ts +44 -0
  52. package/client/src/hooks/useInstallPrompt.ts +53 -0
  53. package/client/src/hooks/useNotifications.ts +54 -0
  54. package/client/src/hooks/useOnline.ts +6 -0
  55. package/client/src/hooks/useSession.ts +110 -4
  56. package/client/src/hooks/useSpinnerVerb.ts +36 -0
  57. package/client/src/hooks/useSwipeDrawer.ts +275 -0
  58. package/client/src/hooks/useVoiceRecorder.ts +123 -0
  59. package/client/src/hooks/useWorkspace.ts +48 -0
  60. package/client/src/providers/WebSocketProvider.tsx +18 -0
  61. package/client/src/router.tsx +48 -20
  62. package/client/src/stores/session.ts +136 -0
  63. package/client/src/stores/sidebar.ts +3 -2
  64. package/client/src/stores/workspace.ts +254 -0
  65. package/client/src/styles/global.css +123 -0
  66. package/client/src/utils/editorUrl.ts +62 -0
  67. package/client/vite.config.ts +53 -1
  68. package/package.json +1 -1
  69. package/server/src/daemon.ts +11 -1
  70. package/server/src/features/scheduler.ts +23 -0
  71. package/server/src/features/sticky-notes.ts +5 -3
  72. package/server/src/handlers/attachment.ts +172 -0
  73. package/server/src/handlers/chat.ts +43 -2
  74. package/server/src/handlers/editor.ts +40 -0
  75. package/server/src/handlers/fs.ts +10 -2
  76. package/server/src/handlers/memory.ts +3 -0
  77. package/server/src/handlers/notes.ts +4 -2
  78. package/server/src/handlers/scheduler.ts +18 -1
  79. package/server/src/handlers/session.ts +14 -8
  80. package/server/src/handlers/settings.ts +37 -2
  81. package/server/src/handlers/terminal.ts +13 -6
  82. package/server/src/project/pty-worker.cjs +83 -0
  83. package/server/src/project/sdk-bridge.ts +266 -11
  84. package/server/src/project/terminal.ts +78 -34
  85. package/shared/src/messages.ts +145 -4
  86. package/shared/src/models.ts +27 -1
  87. package/shared/src/project-settings.ts +1 -1
  88. package/tp.js +19 -0
  89. package/client/public/manifest.json +0 -24
  90. package/client/public/sw.js +0 -61
  91. package/client/src/components/panels/FileBrowser.tsx +0 -241
  92. 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
- title="Coming soon"
328
- disabled
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
- {wasInterrupted && !isProcessing && (
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={function (text) { sendMessage(text, selectedModel, selectedEffort); }}
715
- disabled={isProcessing || !activeSessionId}
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
  }