@cryptiklemur/lattice 1.2.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/.serena/project.yml +138 -0
- 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/ProjectClaude.tsx +14 -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 +24 -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/AddProjectModal.tsx +3 -2
- 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 +131 -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
package/client/index.html
CHANGED
|
@@ -2,12 +2,11 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
|
6
6
|
<meta name="theme-color" content="#0d0d0d" />
|
|
7
7
|
<meta name="apple-mobile-web-app-capable" content="yes" />
|
|
8
8
|
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
|
|
9
9
|
<meta name="apple-mobile-web-app-title" content="Lattice" />
|
|
10
|
-
<link rel="manifest" href="/manifest.json" />
|
|
11
10
|
<link rel="apple-touch-icon" href="/icons/icon-192.svg" />
|
|
12
11
|
<title>Lattice</title>
|
|
13
12
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
@@ -17,16 +16,5 @@
|
|
|
17
16
|
<body>
|
|
18
17
|
<div id="root"></div>
|
|
19
18
|
<script type="module" src="/src/main.tsx"></script>
|
|
20
|
-
<script>
|
|
21
|
-
if ("serviceWorker" in navigator) {
|
|
22
|
-
window.addEventListener("load", function () {
|
|
23
|
-
navigator.serviceWorker.register("/sw.js").then(function (reg) {
|
|
24
|
-
console.log("[lattice] Service worker registered:", reg.scope);
|
|
25
|
-
}).catch(function (err) {
|
|
26
|
-
console.warn("[lattice] Service worker registration failed:", err);
|
|
27
|
-
});
|
|
28
|
-
});
|
|
29
|
-
}
|
|
30
|
-
</script>
|
|
31
19
|
</body>
|
|
32
20
|
</html>
|
package/client/package.json
CHANGED
|
@@ -18,13 +18,17 @@
|
|
|
18
18
|
"@tanstack/react-store": "^0.9.2",
|
|
19
19
|
"@tanstack/react-virtual": "^3.13.23",
|
|
20
20
|
"@xterm/addon-fit": "^0.11.0",
|
|
21
|
+
"@xterm/addon-search": "^0.16.0",
|
|
21
22
|
"@xterm/addon-web-links": "^0.12.0",
|
|
22
23
|
"@xterm/xterm": "^6.0.0",
|
|
24
|
+
"cronstrue": "^3.14.0",
|
|
23
25
|
"daisyui": "^5.5.19",
|
|
24
26
|
"lucide-react": "^0.577.0",
|
|
25
27
|
"react": "^19",
|
|
26
28
|
"react-dom": "^19",
|
|
27
29
|
"react-markdown": "^10.1.0",
|
|
30
|
+
"remark-gfm": "^4.0.1",
|
|
31
|
+
"shiki": "^4.0.2",
|
|
28
32
|
"tailwindcss": "^4.2.2"
|
|
29
33
|
},
|
|
30
34
|
"devDependencies": {
|
|
@@ -32,6 +36,7 @@
|
|
|
32
36
|
"@types/react-dom": "^19",
|
|
33
37
|
"@vitejs/plugin-react": "^6",
|
|
34
38
|
"typescript": "^5.9",
|
|
35
|
-
"vite": "^8"
|
|
39
|
+
"vite": "^8",
|
|
40
|
+
"vite-plugin-pwa": "^1.2.0"
|
|
36
41
|
}
|
|
37
42
|
}
|
package/client/src/App.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import { WebSocketProvider } from "./providers/WebSocketProvider";
|
|
|
4
4
|
import { ErrorBoundary } from "./components/ui/ErrorBoundary";
|
|
5
5
|
import { Toast, useToastState } from "./components/ui/Toast";
|
|
6
6
|
import { CommandPalette } from "./components/ui/CommandPalette";
|
|
7
|
+
import { UpdatePrompt } from "./components/ui/UpdatePrompt";
|
|
7
8
|
|
|
8
9
|
function AppInner() {
|
|
9
10
|
var { items, dismiss } = useToastState();
|
|
@@ -13,6 +14,7 @@ function AppInner() {
|
|
|
13
14
|
<RouterProvider router={router} />
|
|
14
15
|
<CommandPalette />
|
|
15
16
|
<Toast items={items} onDismiss={dismiss} />
|
|
17
|
+
<UpdatePrompt />
|
|
16
18
|
</>
|
|
17
19
|
);
|
|
18
20
|
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type CommandHandler = "client" | "passthrough";
|
|
2
|
+
|
|
3
|
+
export interface SlashCommand {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
aliases?: string[];
|
|
7
|
+
args?: string;
|
|
8
|
+
category: "command" | "skill";
|
|
9
|
+
handler: CommandHandler;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export var builtinCommands: SlashCommand[] = [
|
|
13
|
+
{ name: "clear", description: "Clear conversation, start new session", aliases: ["reset", "new"], category: "command", handler: "client" },
|
|
14
|
+
{ name: "compact", description: "Compact conversation context", args: "[instructions]", category: "command", handler: "passthrough" },
|
|
15
|
+
{ name: "cost", description: "Show token usage and estimated cost", category: "command", handler: "client" },
|
|
16
|
+
{ name: "model", description: "Switch Claude model", args: "[model]", category: "command", handler: "client" },
|
|
17
|
+
{ name: "effort", description: "Set effort level", args: "[low|medium|high|max]", category: "command", handler: "client" },
|
|
18
|
+
{ name: "help", description: "Show available commands", category: "command", handler: "client" },
|
|
19
|
+
{ name: "fast", description: "Toggle fast mode", args: "[on|off]", category: "command", handler: "client" },
|
|
20
|
+
{ name: "copy", description: "Copy last assistant response", category: "command", handler: "client" },
|
|
21
|
+
{ name: "export", description: "Export conversation as text file", category: "command", handler: "client" },
|
|
22
|
+
{ name: "rename", description: "Rename current session", args: "[name]", category: "command", handler: "client" },
|
|
23
|
+
{ name: "context", description: "Show context breakdown", category: "command", handler: "client" },
|
|
24
|
+
{ name: "theme", description: "Open appearance settings", category: "command", handler: "client" },
|
|
25
|
+
{ name: "config", description: "Open settings", aliases: ["settings"], category: "command", handler: "client" },
|
|
26
|
+
{ name: "permissions", description: "Open permissions settings", aliases: ["allowed-tools"], category: "command", handler: "client" },
|
|
27
|
+
{ name: "memory", description: "Open memory settings", category: "command", handler: "client" },
|
|
28
|
+
{ name: "skills", description: "Open skills settings", category: "command", handler: "client" },
|
|
29
|
+
{ name: "plan", description: "Enter plan mode", category: "command", handler: "client" },
|
|
30
|
+
{ name: "diff", description: "Show last git diff", category: "command", handler: "passthrough" },
|
|
31
|
+
{ name: "init", description: "Generate CLAUDE.md", category: "command", handler: "passthrough" },
|
|
32
|
+
{ name: "review", description: "Review code", category: "command", handler: "passthrough" },
|
|
33
|
+
{ name: "pr-comments", description: "Fetch PR comments", args: "[PR]", category: "command", handler: "passthrough" },
|
|
34
|
+
{ name: "security-review", description: "Security review of recent changes", category: "command", handler: "passthrough" },
|
|
35
|
+
{ name: "btw", description: "Ask a side question", args: "<question>", category: "command", handler: "passthrough" },
|
|
36
|
+
];
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { useState } from "react";
|
|
2
|
+
import { X, RotateCcw, Eye, EyeOff } from "lucide-react";
|
|
3
|
+
import type { ClientAttachment } from "../../hooks/useAttachments";
|
|
4
|
+
|
|
5
|
+
interface AttachmentChipsProps {
|
|
6
|
+
attachments: ClientAttachment[];
|
|
7
|
+
onRemove: (id: string) => void;
|
|
8
|
+
onRetry: (id: string) => void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function formatSize(bytes: number): string {
|
|
12
|
+
if (bytes < 1024) return bytes + "B";
|
|
13
|
+
if (bytes < 1024 * 1024) return Math.round(bytes / 1024) + "KB";
|
|
14
|
+
return (bytes / (1024 * 1024)).toFixed(1) + "MB";
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getExtBadge(name: string, type: string): string {
|
|
18
|
+
if (type === "paste") return "TXT";
|
|
19
|
+
var ext = name.split(".").pop()?.toUpperCase() || "";
|
|
20
|
+
return ext.slice(0, 4) || "FILE";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function AttachmentChips(props: AttachmentChipsProps) {
|
|
24
|
+
var [expandedPaste, setExpandedPaste] = useState<string | null>(null);
|
|
25
|
+
|
|
26
|
+
if (props.attachments.length === 0) return null;
|
|
27
|
+
|
|
28
|
+
return (
|
|
29
|
+
<div className="flex flex-wrap gap-1.5 px-3 py-2 border-b border-base-content/8" role="list" aria-label="Attachments">
|
|
30
|
+
{props.attachments.map(function (att) {
|
|
31
|
+
var isFailed = att.status === "failed";
|
|
32
|
+
var isUploading = att.status === "uploading";
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<div key={att.id} role="listitem" className="relative">
|
|
36
|
+
<div
|
|
37
|
+
className={
|
|
38
|
+
"relative flex items-center gap-1.5 rounded-lg px-2 py-1 text-[12px] overflow-hidden transition-colors " +
|
|
39
|
+
(isFailed
|
|
40
|
+
? "bg-error/10 border border-error/30 text-error/80"
|
|
41
|
+
: "bg-base-content/5 border border-base-content/10 text-base-content/70")
|
|
42
|
+
}
|
|
43
|
+
>
|
|
44
|
+
{isUploading && (
|
|
45
|
+
<div
|
|
46
|
+
className="absolute left-0 top-0 bottom-0 bg-primary/10 transition-all duration-300"
|
|
47
|
+
style={{ width: att.progress + "%" }}
|
|
48
|
+
/>
|
|
49
|
+
)}
|
|
50
|
+
|
|
51
|
+
{att.type === "image" && att.previewUrl ? (
|
|
52
|
+
<img
|
|
53
|
+
src={att.previewUrl}
|
|
54
|
+
alt=""
|
|
55
|
+
className="relative w-7 h-7 rounded object-cover flex-shrink-0"
|
|
56
|
+
/>
|
|
57
|
+
) : (
|
|
58
|
+
<span className="relative font-mono text-[10px] text-primary/60 flex-shrink-0">
|
|
59
|
+
{getExtBadge(att.name, att.type)}
|
|
60
|
+
</span>
|
|
61
|
+
)}
|
|
62
|
+
|
|
63
|
+
<span className="relative truncate max-w-[120px]">{att.name}</span>
|
|
64
|
+
|
|
65
|
+
{att.type === "paste" && att.lineCount ? (
|
|
66
|
+
<span className="relative text-[10px] text-base-content/30">{att.lineCount} lines</span>
|
|
67
|
+
) : isUploading ? (
|
|
68
|
+
<span className="relative text-[10px] text-primary/50">{att.progress}%</span>
|
|
69
|
+
) : (
|
|
70
|
+
<span className="relative text-[10px] text-base-content/30">{formatSize(att.size)}</span>
|
|
71
|
+
)}
|
|
72
|
+
|
|
73
|
+
{att.type === "paste" && att.content && (
|
|
74
|
+
<button
|
|
75
|
+
onClick={function () {
|
|
76
|
+
setExpandedPaste(expandedPaste === att.id ? null : att.id);
|
|
77
|
+
}}
|
|
78
|
+
className="relative text-[10px] text-primary/50 hover:text-primary/80 underline"
|
|
79
|
+
aria-label={expandedPaste === att.id ? "Hide preview" : "Show preview"}
|
|
80
|
+
>
|
|
81
|
+
{expandedPaste === att.id ? <EyeOff size={10} /> : <Eye size={10} />}
|
|
82
|
+
</button>
|
|
83
|
+
)}
|
|
84
|
+
|
|
85
|
+
{isFailed && (
|
|
86
|
+
<button
|
|
87
|
+
onClick={function () { props.onRetry(att.id); }}
|
|
88
|
+
className="relative text-error/60 hover:text-error/90"
|
|
89
|
+
aria-label={"Retry " + att.name}
|
|
90
|
+
title={att.error}
|
|
91
|
+
>
|
|
92
|
+
<RotateCcw size={10} />
|
|
93
|
+
</button>
|
|
94
|
+
)}
|
|
95
|
+
|
|
96
|
+
<button
|
|
97
|
+
onClick={function () { props.onRemove(att.id); }}
|
|
98
|
+
className="relative text-base-content/30 hover:text-base-content/60"
|
|
99
|
+
aria-label={"Remove " + att.name}
|
|
100
|
+
>
|
|
101
|
+
<X size={12} />
|
|
102
|
+
</button>
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
{expandedPaste === att.id && att.content && (
|
|
106
|
+
<div className="absolute left-0 bottom-full mb-1 w-[400px] max-h-[200px] overflow-auto rounded-lg border border-base-content/10 bg-base-300 shadow-lg z-50 p-2 font-mono text-[11px] text-base-content/50 whitespace-pre">
|
|
107
|
+
{att.content.slice(0, 2000)}
|
|
108
|
+
{att.content.length > 2000 && "\n... (" + (att.content.length - 2000) + " more characters)"}
|
|
109
|
+
</div>
|
|
110
|
+
)}
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
})}
|
|
114
|
+
</div>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { useRef, useState, useEffect, useMemo } from "react";
|
|
2
|
-
import { SendHorizontal, Settings } from "lucide-react";
|
|
2
|
+
import { SendHorizontal, Settings, Paperclip } from "lucide-react";
|
|
3
3
|
import { useSkills } from "../../hooks/useSkills";
|
|
4
|
+
import { CommandPalette, getFilteredItems } from "./CommandPalette";
|
|
5
|
+
import { useAttachments } from "../../hooks/useAttachments";
|
|
6
|
+
import { useVoiceRecorder } from "../../hooks/useVoiceRecorder";
|
|
7
|
+
import { AttachmentChips } from "./AttachmentChips";
|
|
8
|
+
import { VoiceRecorder } from "./VoiceRecorder";
|
|
4
9
|
|
|
5
10
|
interface ChatInputProps {
|
|
6
|
-
onSend: (text: string) => void;
|
|
11
|
+
onSend: (text: string, attachmentIds: string[]) => void;
|
|
7
12
|
disabled: boolean;
|
|
13
|
+
disabledPlaceholder?: string;
|
|
8
14
|
toolbarContent?: React.ReactNode;
|
|
15
|
+
failedInput?: string | null;
|
|
16
|
+
onFailedInputConsumed?: () => void;
|
|
17
|
+
prefillText?: string | null;
|
|
18
|
+
onPrefillConsumed?: () => void;
|
|
9
19
|
}
|
|
10
20
|
|
|
11
21
|
function getModKey(): string {
|
|
@@ -26,15 +36,19 @@ export function ChatInput(props: ChatInputProps) {
|
|
|
26
36
|
var [showMobileSettings, setShowMobileSettings] = useState(false);
|
|
27
37
|
var modKey = useMemo(getModKey, []);
|
|
28
38
|
|
|
29
|
-
var
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
39
|
+
var attachmentsHook = useAttachments();
|
|
40
|
+
var voice = useVoiceRecorder();
|
|
41
|
+
var [isDragging, setIsDragging] = useState(false);
|
|
42
|
+
var dragCounter = useRef(0);
|
|
43
|
+
var fileInputRef = useRef<HTMLInputElement>(null);
|
|
44
|
+
var savedTextRef = useRef("");
|
|
45
|
+
|
|
46
|
+
var itemCount = useMemo(function () {
|
|
47
|
+
if (slashQuery === null) return 0;
|
|
48
|
+
return getFilteredItems(slashQuery, skills).length;
|
|
35
49
|
}, [slashQuery, skills]);
|
|
36
50
|
|
|
37
|
-
var isOpen = slashQuery !== null &&
|
|
51
|
+
var isOpen = slashQuery !== null && itemCount > 0;
|
|
38
52
|
|
|
39
53
|
useEffect(function () {
|
|
40
54
|
setSelectedIndex(0);
|
|
@@ -60,6 +74,34 @@ export function ChatInput(props: ChatInputProps) {
|
|
|
60
74
|
return function () { document.removeEventListener("mousedown", handleClick); };
|
|
61
75
|
}, [showMobileSettings]);
|
|
62
76
|
|
|
77
|
+
useEffect(function () {
|
|
78
|
+
if (props.failedInput && textareaRef.current) {
|
|
79
|
+
var el = textareaRef.current;
|
|
80
|
+
el.value = props.failedInput;
|
|
81
|
+
el.style.height = "auto";
|
|
82
|
+
el.style.height = Math.min(el.scrollHeight, 160) + "px";
|
|
83
|
+
el.focus();
|
|
84
|
+
checkSlash();
|
|
85
|
+
if (props.onFailedInputConsumed) {
|
|
86
|
+
props.onFailedInputConsumed();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}, [props.failedInput]);
|
|
90
|
+
|
|
91
|
+
useEffect(function () {
|
|
92
|
+
if (props.prefillText && textareaRef.current) {
|
|
93
|
+
var el = textareaRef.current;
|
|
94
|
+
el.value = props.prefillText;
|
|
95
|
+
el.style.height = "auto";
|
|
96
|
+
el.style.height = Math.min(el.scrollHeight, 160) + "px";
|
|
97
|
+
el.focus();
|
|
98
|
+
checkSlash();
|
|
99
|
+
if (props.onPrefillConsumed) {
|
|
100
|
+
props.onPrefillConsumed();
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}, [props.prefillText]);
|
|
104
|
+
|
|
63
105
|
function checkSlash() {
|
|
64
106
|
var el = textareaRef.current;
|
|
65
107
|
if (!el) return;
|
|
@@ -71,30 +113,42 @@ export function ChatInput(props: ChatInputProps) {
|
|
|
71
113
|
}
|
|
72
114
|
}
|
|
73
115
|
|
|
74
|
-
function
|
|
116
|
+
function selectItem(item: { name: string; args?: string; category: string; handler: string }) {
|
|
75
117
|
var el = textareaRef.current;
|
|
76
118
|
if (!el) return;
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
119
|
+
|
|
120
|
+
var hasArgs = !!item.args;
|
|
121
|
+
var isSkill = item.category === "skill";
|
|
122
|
+
|
|
123
|
+
if (hasArgs || isSkill) {
|
|
124
|
+
el.value = "/" + item.name + " ";
|
|
125
|
+
el.focus();
|
|
126
|
+
setSlashQuery(null);
|
|
127
|
+
} else {
|
|
128
|
+
props.onSend("/" + item.name, []);
|
|
129
|
+
el.value = "";
|
|
130
|
+
el.style.height = "auto";
|
|
131
|
+
setSlashQuery(null);
|
|
132
|
+
}
|
|
80
133
|
}
|
|
81
134
|
|
|
82
135
|
function handleKeyDown(e: React.KeyboardEvent<HTMLTextAreaElement>) {
|
|
83
136
|
if (isOpen) {
|
|
84
137
|
if (e.key === "ArrowUp") {
|
|
85
138
|
e.preventDefault();
|
|
86
|
-
setSelectedIndex(function (i) { return i > 0 ? i - 1 :
|
|
139
|
+
setSelectedIndex(function (i) { return i > 0 ? i - 1 : itemCount - 1; });
|
|
87
140
|
return;
|
|
88
141
|
}
|
|
89
142
|
if (e.key === "ArrowDown") {
|
|
90
143
|
e.preventDefault();
|
|
91
|
-
setSelectedIndex(function (i) { return i <
|
|
144
|
+
setSelectedIndex(function (i) { return i < itemCount - 1 ? i + 1 : 0; });
|
|
92
145
|
return;
|
|
93
146
|
}
|
|
94
147
|
if (e.key === "Tab" || (e.key === "Enter" && !e.shiftKey)) {
|
|
95
148
|
e.preventDefault();
|
|
96
|
-
|
|
97
|
-
|
|
149
|
+
var items = getFilteredItems(slashQuery!, skills);
|
|
150
|
+
if (items[selectedIndex]) {
|
|
151
|
+
selectItem(items[selectedIndex]);
|
|
98
152
|
}
|
|
99
153
|
return;
|
|
100
154
|
}
|
|
@@ -117,54 +171,112 @@ export function ChatInput(props: ChatInputProps) {
|
|
|
117
171
|
checkSlash();
|
|
118
172
|
}
|
|
119
173
|
|
|
174
|
+
function handlePaste(e: React.ClipboardEvent<HTMLTextAreaElement>) {
|
|
175
|
+
var items = e.clipboardData.items;
|
|
176
|
+
for (var i = 0; i < items.length; i++) {
|
|
177
|
+
if (items[i].type.startsWith("image/")) {
|
|
178
|
+
e.preventDefault();
|
|
179
|
+
var file = items[i].getAsFile();
|
|
180
|
+
if (file && attachmentsHook.canAttach) {
|
|
181
|
+
attachmentsHook.addFile(file);
|
|
182
|
+
}
|
|
183
|
+
return;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
var text = e.clipboardData.getData("text/plain");
|
|
188
|
+
if (text && text.split("\n").length >= 10) {
|
|
189
|
+
e.preventDefault();
|
|
190
|
+
if (attachmentsHook.canAttach) {
|
|
191
|
+
attachmentsHook.addPaste(text);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function handleDragEnter(e: React.DragEvent) {
|
|
197
|
+
e.preventDefault();
|
|
198
|
+
dragCounter.current++;
|
|
199
|
+
if (e.dataTransfer.types.indexOf("Files") !== -1) {
|
|
200
|
+
setIsDragging(true);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function handleDragLeave(e: React.DragEvent) {
|
|
205
|
+
e.preventDefault();
|
|
206
|
+
dragCounter.current--;
|
|
207
|
+
if (dragCounter.current === 0) {
|
|
208
|
+
setIsDragging(false);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function handleDragOver(e: React.DragEvent) {
|
|
213
|
+
e.preventDefault();
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function handleDrop(e: React.DragEvent) {
|
|
217
|
+
e.preventDefault();
|
|
218
|
+
dragCounter.current = 0;
|
|
219
|
+
setIsDragging(false);
|
|
220
|
+
var files = e.dataTransfer.files;
|
|
221
|
+
for (var i = 0; i < files.length; i++) {
|
|
222
|
+
if (attachmentsHook.canAttach) {
|
|
223
|
+
attachmentsHook.addFile(files[i]);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function handleVoiceStart() {
|
|
229
|
+
savedTextRef.current = textareaRef.current?.value || "";
|
|
230
|
+
voice.start();
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function handleVoiceStop() {
|
|
234
|
+
var transcript = voice.stop();
|
|
235
|
+
if (transcript && textareaRef.current) {
|
|
236
|
+
var el = textareaRef.current;
|
|
237
|
+
var existing = savedTextRef.current;
|
|
238
|
+
el.value = existing ? existing + " " + transcript : transcript;
|
|
239
|
+
el.style.height = "auto";
|
|
240
|
+
el.style.height = Math.min(el.scrollHeight, 160) + "px";
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function handleVoiceCancel() {
|
|
245
|
+
voice.cancel();
|
|
246
|
+
if (textareaRef.current) {
|
|
247
|
+
textareaRef.current.value = savedTextRef.current;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
120
251
|
function submit() {
|
|
121
252
|
var el = textareaRef.current;
|
|
122
|
-
if (!el)
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
253
|
+
if (!el) return;
|
|
125
254
|
var text = el.value.trim();
|
|
126
|
-
if (!text || props.disabled)
|
|
127
|
-
|
|
128
|
-
}
|
|
129
|
-
props.onSend(text);
|
|
255
|
+
if ((!text && attachmentsHook.attachments.length === 0) || props.disabled || attachmentsHook.hasUploading) return;
|
|
256
|
+
props.onSend(text, attachmentsHook.readyIds);
|
|
130
257
|
el.value = "";
|
|
131
258
|
el.style.height = "auto";
|
|
132
259
|
setSlashQuery(null);
|
|
260
|
+
attachmentsHook.clearAll();
|
|
133
261
|
}
|
|
134
262
|
|
|
135
263
|
return (
|
|
136
|
-
<div
|
|
264
|
+
<div
|
|
265
|
+
className="relative"
|
|
266
|
+
onDragEnter={handleDragEnter}
|
|
267
|
+
onDragLeave={handleDragLeave}
|
|
268
|
+
onDragOver={handleDragOver}
|
|
269
|
+
onDrop={handleDrop}
|
|
270
|
+
>
|
|
137
271
|
{isOpen && (
|
|
138
|
-
<div
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
<button
|
|
147
|
-
key={skill.name}
|
|
148
|
-
data-active={i === selectedIndex}
|
|
149
|
-
onMouseDown={function (e) {
|
|
150
|
-
e.preventDefault();
|
|
151
|
-
selectSkill(skill.name);
|
|
152
|
-
}}
|
|
153
|
-
onMouseEnter={function () { setSelectedIndex(i); }}
|
|
154
|
-
className={
|
|
155
|
-
"flex w-full items-center gap-3 px-3.5 py-2.5 text-left transition-colors " +
|
|
156
|
-
(i === selectedIndex ? "bg-primary/10" : "hover:bg-base-content/5")
|
|
157
|
-
}
|
|
158
|
-
>
|
|
159
|
-
<span className="font-mono text-[12px] text-primary/90 whitespace-nowrap flex-shrink-0">
|
|
160
|
-
/{skill.name}
|
|
161
|
-
</span>
|
|
162
|
-
<span className="text-[11px] text-base-content/40 truncate min-w-0">
|
|
163
|
-
{skill.description}
|
|
164
|
-
</span>
|
|
165
|
-
</button>
|
|
166
|
-
);
|
|
167
|
-
})}
|
|
272
|
+
<div ref={popupRef}>
|
|
273
|
+
<CommandPalette
|
|
274
|
+
query={slashQuery!}
|
|
275
|
+
skills={skills}
|
|
276
|
+
selectedIndex={selectedIndex}
|
|
277
|
+
onSelect={selectItem}
|
|
278
|
+
onHover={setSelectedIndex}
|
|
279
|
+
/>
|
|
168
280
|
</div>
|
|
169
281
|
)}
|
|
170
282
|
|
|
@@ -179,12 +291,32 @@ export function ChatInput(props: ChatInputProps) {
|
|
|
179
291
|
</div>
|
|
180
292
|
)}
|
|
181
293
|
|
|
294
|
+
<input
|
|
295
|
+
ref={fileInputRef}
|
|
296
|
+
type="file"
|
|
297
|
+
multiple
|
|
298
|
+
className="hidden"
|
|
299
|
+
onChange={function (e) {
|
|
300
|
+
var files = e.target.files;
|
|
301
|
+
if (files) {
|
|
302
|
+
for (var i = 0; i < files.length; i++) {
|
|
303
|
+
if (attachmentsHook.canAttach) {
|
|
304
|
+
attachmentsHook.addFile(files[i]);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
e.target.value = "";
|
|
309
|
+
}}
|
|
310
|
+
/>
|
|
311
|
+
|
|
182
312
|
<div
|
|
183
313
|
className={
|
|
184
314
|
"border rounded-xl bg-base-300/60 overflow-hidden transition-all duration-150 " +
|
|
185
|
-
(
|
|
186
|
-
? "border-
|
|
187
|
-
:
|
|
315
|
+
(isDragging
|
|
316
|
+
? "border-primary/40 shadow-[0_0_0_3px_oklch(from_var(--color-primary)_l_c_h/0.1)]"
|
|
317
|
+
: props.disabled
|
|
318
|
+
? "border-base-content/10 opacity-60"
|
|
319
|
+
: "border-primary/20 focus-within:border-primary/40 focus-within:shadow-[0_0_0_3px_oklch(from_var(--color-primary)_l_c_h/0.1)]")
|
|
188
320
|
}
|
|
189
321
|
>
|
|
190
322
|
<div className="hidden sm:flex items-center gap-1.5 px-3 py-1.5 border-b border-base-content/8 font-mono text-[10px]">
|
|
@@ -192,24 +324,62 @@ export function ChatInput(props: ChatInputProps) {
|
|
|
192
324
|
<span className="flex-1" />
|
|
193
325
|
<span className="text-base-content/20">{modKey}+K commands</span>
|
|
194
326
|
</div>
|
|
327
|
+
|
|
328
|
+
<AttachmentChips
|
|
329
|
+
attachments={attachmentsHook.attachments}
|
|
330
|
+
onRemove={attachmentsHook.removeAttachment}
|
|
331
|
+
onRetry={attachmentsHook.retryAttachment}
|
|
332
|
+
/>
|
|
333
|
+
|
|
195
334
|
<div className="flex items-center gap-2 px-3.5 py-2.5">
|
|
196
|
-
<div className="flex-1
|
|
197
|
-
<
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
placeholder={props.disabled ? "Claude is responding..." : "Message Claude..."}
|
|
202
|
-
disabled={props.disabled}
|
|
203
|
-
onKeyDown={handleKeyDown}
|
|
204
|
-
onInput={handleInput}
|
|
205
|
-
rows={1}
|
|
206
|
-
style={{ padding: "1px 0 0 16px", margin: 0, border: "none" }}
|
|
335
|
+
<div className="flex gap-1 flex-shrink-0">
|
|
336
|
+
<button
|
|
337
|
+
aria-label="Attach file"
|
|
338
|
+
disabled={!attachmentsHook.canAttach}
|
|
339
|
+
onClick={function () { fileInputRef.current?.click(); }}
|
|
207
340
|
className={
|
|
208
|
-
"w-
|
|
209
|
-
(
|
|
341
|
+
"w-7 h-7 rounded-md flex items-center justify-center transition-colors " +
|
|
342
|
+
(attachmentsHook.canAttach
|
|
343
|
+
? "text-base-content/30 hover:text-base-content/50 border border-base-content/10 hover:border-base-content/20"
|
|
344
|
+
: "text-base-content/15 cursor-not-allowed")
|
|
210
345
|
}
|
|
346
|
+
title={attachmentsHook.canAttach ? "Attach file" : "Maximum attachments reached"}
|
|
347
|
+
>
|
|
348
|
+
<Paperclip size={13} />
|
|
349
|
+
</button>
|
|
350
|
+
<VoiceRecorder
|
|
351
|
+
isRecording={voice.isRecording}
|
|
352
|
+
isSupported={voice.isSupported}
|
|
353
|
+
isSpeaking={voice.isSpeaking}
|
|
354
|
+
elapsed={voice.elapsed}
|
|
355
|
+
interimTranscript={voice.interimTranscript}
|
|
356
|
+
onStart={handleVoiceStart}
|
|
357
|
+
onStop={handleVoiceStop}
|
|
358
|
+
onCancel={handleVoiceCancel}
|
|
211
359
|
/>
|
|
212
360
|
</div>
|
|
361
|
+
|
|
362
|
+
{voice.isRecording ? null : (
|
|
363
|
+
<div className="flex-1 min-w-0 relative">
|
|
364
|
+
<span className="absolute left-0 top-[1px] text-primary/50 font-mono text-[14px] leading-relaxed select-none pointer-events-none">›</span>
|
|
365
|
+
<textarea
|
|
366
|
+
ref={textareaRef}
|
|
367
|
+
aria-label="Message input"
|
|
368
|
+
placeholder={props.disabled ? (props.disabledPlaceholder || "Claude is responding...") : "Message Claude..."}
|
|
369
|
+
disabled={props.disabled}
|
|
370
|
+
onKeyDown={handleKeyDown}
|
|
371
|
+
onInput={handleInput}
|
|
372
|
+
onPaste={handlePaste}
|
|
373
|
+
rows={1}
|
|
374
|
+
style={{ padding: "1px 0 0 16px", margin: 0, border: "none" }}
|
|
375
|
+
className={
|
|
376
|
+
"w-full resize-none bg-transparent text-base-content text-[14px] leading-relaxed max-h-[160px] overflow-y-auto outline-none placeholder:text-base-content/30 " +
|
|
377
|
+
(props.disabled ? "cursor-not-allowed" : "cursor-text")
|
|
378
|
+
}
|
|
379
|
+
/>
|
|
380
|
+
</div>
|
|
381
|
+
)}
|
|
382
|
+
|
|
213
383
|
<div className="flex items-center gap-1.5 flex-shrink-0">
|
|
214
384
|
<button
|
|
215
385
|
ref={settingsBtnRef}
|
|
@@ -222,11 +392,12 @@ export function ChatInput(props: ChatInputProps) {
|
|
|
222
392
|
<span className="text-[10px] text-base-content/20 font-mono hidden sm:block">⏎ send</span>
|
|
223
393
|
<button
|
|
224
394
|
aria-label="Send message"
|
|
225
|
-
disabled={props.disabled}
|
|
395
|
+
disabled={props.disabled || attachmentsHook.hasUploading}
|
|
226
396
|
onClick={submit}
|
|
397
|
+
title={attachmentsHook.hasUploading ? "Uploading..." : "Send message"}
|
|
227
398
|
className={
|
|
228
399
|
"w-8 h-8 rounded-lg flex items-center justify-center flex-shrink-0 transition-all duration-150 outline-none " +
|
|
229
|
-
(props.disabled
|
|
400
|
+
(props.disabled || attachmentsHook.hasUploading
|
|
230
401
|
? "bg-base-content/5 text-base-content/20 cursor-not-allowed"
|
|
231
402
|
: "bg-primary text-primary-content hover:bg-primary/80 cursor-pointer focus-visible:ring-2 focus-visible:ring-primary/50 focus-visible:ring-offset-2 focus-visible:ring-offset-base-300")
|
|
232
403
|
}
|
|
@@ -236,6 +407,12 @@ export function ChatInput(props: ChatInputProps) {
|
|
|
236
407
|
</div>
|
|
237
408
|
</div>
|
|
238
409
|
</div>
|
|
410
|
+
|
|
411
|
+
{isDragging && (
|
|
412
|
+
<div className="absolute inset-0 rounded-xl border-2 border-dashed border-primary/40 bg-primary/5 flex items-center justify-center z-40 pointer-events-none">
|
|
413
|
+
<span className="text-[13px] text-primary/60 font-mono">Drop files to attach</span>
|
|
414
|
+
</div>
|
|
415
|
+
)}
|
|
239
416
|
</div>
|
|
240
417
|
);
|
|
241
418
|
}
|