shraga 0.0.3 → 0.1.2
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/README.md +82 -27
- package/defaults/agents/summarizer.md +16 -0
- package/defaults/agents/trace-extractor.md +84 -0
- package/defaults/bin/claude +45 -0
- package/defaults/bin/claude-revive +17 -0
- package/defaults/extensions/README.md +70 -0
- package/defaults/extensions/selftest.ext.ts +43 -0
- package/defaults/extensions/stripe-webhook.ext.ts +58 -0
- package/defaults/gmail-triage-prompt.md +42 -0
- package/defaults/scripts/README +4 -0
- package/defaults/scripts/agent-once.ts +67 -0
- package/defaults/scripts/backfill-slack-usernames.ts +82 -0
- package/defaults/scripts/notifier-throttle.ts +44 -0
- package/defaults/scripts/summarize-conversations.ts +5 -0
- package/defaults/shraga.config.ts +29 -0
- package/defaults/skills/add-skill.md +14 -0
- package/defaults/skills/artifacts.md +116 -0
- package/defaults/skills/code-review.md +26 -0
- package/defaults/skills/communications.md +54 -0
- package/defaults/skills/context-audit.md +87 -0
- package/defaults/skills/debug.md +10 -0
- package/defaults/skills/garden.md +179 -0
- package/defaults/skills/github-contributor.md +35 -0
- package/defaults/skills/identity.md +30 -0
- package/defaults/skills/mcp-server.md +62 -0
- package/defaults/skills/mcps-sync.md +105 -0
- package/defaults/skills/plan.md +9 -0
- package/defaults/skills/platform.md +177 -0
- package/defaults/skills/reconcile.md +239 -0
- package/defaults/skills/scheduler.md +192 -0
- package/defaults/skills/self-aware.md +136 -0
- package/defaults/skills/shraga-know.md +333 -0
- package/defaults/skills/stripe.md +55 -0
- package/defaults/skills/write-tests.md +10 -0
- package/defaults/skills-defaults.json +1 -0
- package/defaults/system-prompt.md +46 -0
- package/defaults/workspace/context.md +28 -0
- package/defaults/workspace.md +50 -0
- package/defaults/zdotdir/.gitignore +8 -0
- package/defaults/zdotdir/.zlogin +3 -0
- package/defaults/zdotdir/.zprofile +1 -0
- package/defaults/zdotdir/.zshenv +4 -0
- package/defaults/zdotdir/.zshrc +3 -0
- package/dist/client/assets/index-BoHttkMt.js +1940 -0
- package/dist/client/assets/index-DdibEb2O.css +10 -0
- package/dist/client/index.html +22 -0
- package/package.json +59 -14
- package/src/cli.ts +71 -46
- package/src/client/App.tsx +510 -0
- package/src/client/components/ArtifactCard.tsx +26 -0
- package/src/client/components/ArtifactPanel.tsx +138 -0
- package/src/client/components/AuthedImage.tsx +85 -0
- package/src/client/components/AutocompleteTextarea.tsx +149 -0
- package/src/client/components/ChatView.tsx +866 -0
- package/src/client/components/CliAuthConsent.tsx +98 -0
- package/src/client/components/ConfigPanel.tsx +328 -0
- package/src/client/components/ConversationHeader.tsx +156 -0
- package/src/client/components/ConversationPane.tsx +277 -0
- package/src/client/components/LoginPage.tsx +81 -0
- package/src/client/components/MachineStats.tsx +77 -0
- package/src/client/components/McpManager.tsx +209 -0
- package/src/client/components/MessageInput.tsx +263 -0
- package/src/client/components/OAuthConsent.tsx +103 -0
- package/src/client/components/SchedulesManager.tsx +99 -0
- package/src/client/components/Sidebar.tsx +235 -0
- package/src/client/components/SkillsManager.tsx +280 -0
- package/src/client/components/SmartChart.tsx +167 -0
- package/src/client/components/Toast.tsx +54 -0
- package/src/client/components/WorkspaceTree.tsx +313 -0
- package/src/client/components/ZoomableImage.tsx +123 -0
- package/src/client/components/artifact-presets.ts +10 -0
- package/src/client/components/schedules/ScheduleEditor.tsx +264 -0
- package/src/client/components/schedules/ScheduleList.tsx +271 -0
- package/src/client/components/ui/accordion.tsx +50 -0
- package/src/client/components/ui/button.tsx +43 -0
- package/src/client/components/ui/dialog.tsx +82 -0
- package/src/client/components/ui/input.tsx +19 -0
- package/src/client/components/ui/scroll-area.tsx +39 -0
- package/src/client/components/ui/textarea.tsx +18 -0
- package/src/client/globals.css +51 -0
- package/src/client/hooks/useAgentSocket.ts +79 -0
- package/src/client/hooks/useArtifacts.ts +89 -0
- package/src/client/hooks/useAuth.ts +127 -0
- package/src/client/hooks/useConversation.ts +412 -0
- package/src/client/hooks/useDarkMode.ts +57 -0
- package/src/client/hooks/useIsMobile.ts +23 -0
- package/src/client/hooks/usePush.ts +127 -0
- package/src/client/hooks/useSchedules.ts +73 -0
- package/src/client/hooks/useUnread.ts +238 -0
- package/src/client/lib/desktopAttention.ts +75 -0
- package/src/client/lib/firebase.ts +32 -0
- package/src/client/lib/googleAuthNative.ts +94 -0
- package/src/client/lib/native.ts +43 -0
- package/src/client/lib/schedule-types.ts +34 -0
- package/src/client/lib/sessionApi.ts +58 -0
- package/src/client/lib/slots.tsx +79 -0
- package/src/client/lib/storage.ts +39 -0
- package/src/client/lib/utils.ts +26 -0
- package/src/client/lib/workspaceContext.tsx +54 -0
- package/src/client/lib/ws.ts +203 -0
- package/src/client/main.tsx +14 -0
- package/src/mcp-stdio-bridge.ts +70 -0
- package/src/scripts/summarize-conversations.ts +5 -0
- package/src/scripts/typecheck.ts +43 -0
- package/src/server/agents.ts +54 -0
- package/src/server/api-keys.ts +63 -0
- package/src/server/artifacts/artifacts.export.ts +85 -0
- package/src/server/artifacts/artifacts.handler.ts +93 -0
- package/src/server/artifacts/artifacts.routes.ts +43 -0
- package/src/server/artifacts/artifacts.service.ts +100 -0
- package/src/server/artifacts/artifacts.types.ts +31 -0
- package/src/server/auth.ts +262 -0
- package/src/server/claude.ts +394 -0
- package/src/server/commands.ts +21 -0
- package/src/server/contacts.ts +177 -0
- package/src/server/conversation-summarizer.ts +204 -0
- package/src/server/data-sync.ts +664 -0
- package/src/server/directives.ts +91 -0
- package/src/server/engine/claude-code.ts +514 -0
- package/src/server/engine/index.ts +41 -0
- package/src/server/engine/registry.ts +21 -0
- package/src/server/engine/shared.ts +47 -0
- package/src/server/engine/types.ts +48 -0
- package/src/server/env-resolve.ts +71 -0
- package/src/server/env-sanitize.ts +9 -0
- package/src/server/events/bus.ts +29 -0
- package/src/server/events/dispatcher.ts +48 -0
- package/src/server/events/routes.ts +19 -0
- package/src/server/events/types.ts +9 -0
- package/src/server/extensions.ts +101 -0
- package/src/server/features.ts +109 -0
- package/src/server/file-inject.ts +45 -0
- package/src/server/hooks.ts +142 -0
- package/src/server/idempotency.ts +25 -0
- package/src/server/index.ts +1715 -0
- package/src/server/integrity-audit.ts +132 -0
- package/src/server/mcp-catalog.ts +70 -0
- package/src/server/mcp-oauth.ts +198 -0
- package/src/server/mcp-progress.ts +45 -0
- package/src/server/mcp-server.ts +456 -0
- package/src/server/mcp-sidecar.ts +87 -0
- package/src/server/mcp.ts +291 -0
- package/src/server/model-aliases.ts +76 -0
- package/src/server/paths.ts +24 -0
- package/src/server/polls.ts +175 -0
- package/src/server/push/apns.ts +113 -0
- package/src/server/push/fcm.ts +108 -0
- package/src/server/push/push.ts +66 -0
- package/src/server/push/store.ts +84 -0
- package/src/server/push/triggers.ts +99 -0
- package/src/server/scheduler/builtins.ts +157 -0
- package/src/server/scheduler/engine.ts +432 -0
- package/src/server/scheduler/index.ts +4 -0
- package/src/server/scheduler/runner.ts +334 -0
- package/src/server/scheduler/storage.ts +98 -0
- package/src/server/scheduler/timing.ts +70 -0
- package/src/server/scheduler/types.ts +62 -0
- package/src/server/sdk-utils.ts +45 -0
- package/src/server/seed.ts +174 -0
- package/src/server/session-bus.ts +18 -0
- package/src/server/sessions.ts +559 -0
- package/src/server/shraga-config.ts +167 -0
- package/src/server/skills.ts +372 -0
- package/src/server/slack/api.ts +37 -0
- package/src/server/slack/bot.ts +391 -0
- package/src/server/slack/context-cache.ts +42 -0
- package/src/server/slack/feature.ts +59 -0
- package/src/server/slack/mention-rewrite.ts +59 -0
- package/src/server/slack/oauth.ts +102 -0
- package/src/server/slack/questions.ts +112 -0
- package/src/server/slack/sessions.ts +139 -0
- package/src/server/stats.ts +106 -0
- package/src/server/summarize.ts +11 -0
- package/src/server/turn-context.ts +61 -0
- package/src/server/unclaw-config.ts +19 -0
- package/src/server/unread.ts +79 -0
- package/src/server/user-context.ts +33 -0
- package/src/server/vendor-sync.ts +52 -0
- package/src/server/voice-provider.ts +74 -0
- package/src/server/workspace.ts +249 -0
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState } from 'react';
|
|
2
|
+
import { SendHorizontal, Paperclip, X, Square, FileText } from 'lucide-react';
|
|
3
|
+
import { Button } from './ui/button';
|
|
4
|
+
import { AutocompleteTextarea, type AutocompleteTextareaHandle } from './AutocompleteTextarea';
|
|
5
|
+
import { AuthedImage } from './AuthedImage';
|
|
6
|
+
import { useSlots } from '@/lib/slots';
|
|
7
|
+
import type { Attachment } from '@/hooks/useConversation';
|
|
8
|
+
|
|
9
|
+
export interface MessageInputHandle {
|
|
10
|
+
prefill: (text: string) => void;
|
|
11
|
+
/** Set the input text without stealing focus (used for live external transcription). */
|
|
12
|
+
setText: (text: string) => void;
|
|
13
|
+
focus: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const DRAFT_PREFIX = 'shraga:draft:';
|
|
17
|
+
|
|
18
|
+
function saveDraft(sessionId: string | undefined, text: string) {
|
|
19
|
+
const key = DRAFT_PREFIX + (sessionId ?? 'new');
|
|
20
|
+
if (text.trim()) localStorage.setItem(key, text);
|
|
21
|
+
else localStorage.removeItem(key);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function loadDraft(sessionId: string | undefined): string {
|
|
25
|
+
return localStorage.getItem(DRAFT_PREFIX + (sessionId ?? 'new')) ?? '';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function clearDraft(sessionId: string | undefined) {
|
|
29
|
+
localStorage.removeItem(DRAFT_PREFIX + (sessionId ?? 'new'));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface Props {
|
|
33
|
+
onSend: (text: string, attachments?: Attachment[]) => void;
|
|
34
|
+
onSteer: (text: string) => void;
|
|
35
|
+
onQueue: (text: string) => void;
|
|
36
|
+
onRemoveQueued: (index: number) => void;
|
|
37
|
+
queue: string[];
|
|
38
|
+
onUpload: (file: File) => Promise<Attachment | null>;
|
|
39
|
+
onCancel: () => void;
|
|
40
|
+
disabled?: boolean;
|
|
41
|
+
busy?: boolean;
|
|
42
|
+
skills?: string[];
|
|
43
|
+
workspaceFiles?: string[];
|
|
44
|
+
sessionId?: string;
|
|
45
|
+
/** Opaque per-pane handle passed straight through to the inputAdornments slot; unused in core. */
|
|
46
|
+
inputCtx?: unknown;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const MessageInput = forwardRef<MessageInputHandle, Props>(function MessageInput(
|
|
50
|
+
{ onSend, onSteer, onQueue, onRemoveQueued, queue, onUpload, onCancel, disabled, busy, skills = [], workspaceFiles = [], sessionId, inputCtx },
|
|
51
|
+
ref,
|
|
52
|
+
) {
|
|
53
|
+
const slots = useSlots();
|
|
54
|
+
const [text, setText] = useState(() => loadDraft(sessionId));
|
|
55
|
+
const [attachments, setAttachments] = useState<Attachment[]>([]);
|
|
56
|
+
const [uploading, setUploading] = useState(false);
|
|
57
|
+
const fileRef = useRef<HTMLInputElement>(null);
|
|
58
|
+
const acRef = useRef<AutocompleteTextareaHandle>(null);
|
|
59
|
+
const sessionIdRef = useRef(sessionId);
|
|
60
|
+
|
|
61
|
+
useEffect(() => {
|
|
62
|
+
if (sessionIdRef.current === sessionId) return;
|
|
63
|
+
const prevId = sessionIdRef.current;
|
|
64
|
+
sessionIdRef.current = sessionId;
|
|
65
|
+
// New chat: carry current draft forward instead of saving to old session
|
|
66
|
+
if (!sessionId) {
|
|
67
|
+
clearDraft(prevId);
|
|
68
|
+
saveDraft(undefined, text);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
saveDraft(prevId, text);
|
|
72
|
+
const restored = loadDraft(sessionId);
|
|
73
|
+
setText(restored);
|
|
74
|
+
acRef.current?.setValue(restored);
|
|
75
|
+
}, [sessionId]);
|
|
76
|
+
|
|
77
|
+
const setTextAndSave = useCallback((value: string) => {
|
|
78
|
+
setText(value);
|
|
79
|
+
saveDraft(sessionIdRef.current, value);
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
useImperativeHandle(ref, () => ({
|
|
83
|
+
prefill(value: string) {
|
|
84
|
+
setTextAndSave(value);
|
|
85
|
+
acRef.current?.setValue(value);
|
|
86
|
+
acRef.current?.focus();
|
|
87
|
+
},
|
|
88
|
+
setText(value: string) {
|
|
89
|
+
setTextAndSave(value);
|
|
90
|
+
acRef.current?.setValue(value);
|
|
91
|
+
},
|
|
92
|
+
focus() {
|
|
93
|
+
acRef.current?.focus();
|
|
94
|
+
},
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
const submit = (mode: 'send' | 'steer' | 'queue' = 'send') => {
|
|
98
|
+
const trimmed = text.trim();
|
|
99
|
+
if (!trimmed && attachments.length === 0) return;
|
|
100
|
+
if (mode === 'steer') onSteer(trimmed);
|
|
101
|
+
else if (mode === 'queue') onQueue(trimmed);
|
|
102
|
+
else onSend(trimmed, attachments.length > 0 ? attachments : undefined);
|
|
103
|
+
setText('');
|
|
104
|
+
clearDraft(sessionIdRef.current);
|
|
105
|
+
setAttachments([]);
|
|
106
|
+
acRef.current?.setValue('');
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
// Mobile = coarse primary pointer (phone/tablet). Touchscreen laptops keep a fine pointer, so Enter still submits there.
|
|
110
|
+
const isTouchDevice = typeof window !== 'undefined' && window.matchMedia?.('(pointer: coarse)').matches;
|
|
111
|
+
|
|
112
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
113
|
+
if (e.key === 'Enter' && !e.shiftKey && (!isTouchDevice || e.metaKey || e.ctrlKey)) {
|
|
114
|
+
e.preventDefault();
|
|
115
|
+
submit(busy ? 'steer' : 'send');
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (e.key === 'Tab' && busy && text.trim()) {
|
|
119
|
+
e.preventDefault();
|
|
120
|
+
submit('queue');
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const uploadFile = useCallback(async (file: File) => {
|
|
126
|
+
setUploading(true);
|
|
127
|
+
try {
|
|
128
|
+
const att = await onUpload(file);
|
|
129
|
+
if (att) setAttachments((prev) => [...prev, att]);
|
|
130
|
+
} finally {
|
|
131
|
+
setUploading(false);
|
|
132
|
+
}
|
|
133
|
+
}, [onUpload]);
|
|
134
|
+
|
|
135
|
+
const handlePaste = useCallback((e: React.ClipboardEvent) => {
|
|
136
|
+
const items = Array.from(e.clipboardData.items);
|
|
137
|
+
for (const item of items) {
|
|
138
|
+
if (item.type.startsWith('image/') || item.type === 'application/pdf') {
|
|
139
|
+
const file = item.getAsFile();
|
|
140
|
+
if (file) uploadFile(file);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}, [uploadFile]);
|
|
144
|
+
|
|
145
|
+
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
146
|
+
const file = e.target.files?.[0];
|
|
147
|
+
if (!file) return;
|
|
148
|
+
uploadFile(file);
|
|
149
|
+
e.target.value = '';
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
return (
|
|
153
|
+
// paddingBottom: the input's own background continues into the iOS home-indicator strip (no-op on web).
|
|
154
|
+
<div className="border-t bg-background" style={{ paddingBottom: 'env(safe-area-inset-bottom)' }}>
|
|
155
|
+
<div className="max-w-3xl mx-auto px-2 sm:px-4 py-3">
|
|
156
|
+
{(attachments.length > 0 || uploading) && (
|
|
157
|
+
<div className="flex gap-2 mb-2 flex-wrap items-center">
|
|
158
|
+
{attachments.map((att, i) => (
|
|
159
|
+
<div key={i} className="relative group">
|
|
160
|
+
{att.mimeType.startsWith('image/') ? (
|
|
161
|
+
<AuthedImage src={att.url} alt={att.name} className="h-16 w-16 object-cover rounded-lg border" />
|
|
162
|
+
) : (
|
|
163
|
+
<div className="h-16 px-3 flex items-center gap-2 rounded-lg border bg-muted/50 text-xs text-muted-foreground">
|
|
164
|
+
<FileText className="w-4 h-4 shrink-0" />
|
|
165
|
+
<span className="truncate max-w-[120px]">{att.name}</span>
|
|
166
|
+
</div>
|
|
167
|
+
)}
|
|
168
|
+
<button
|
|
169
|
+
onClick={() => setAttachments((prev) => prev.filter((_, j) => j !== i))}
|
|
170
|
+
className="absolute -top-1.5 -right-1.5 bg-destructive text-destructive-foreground rounded-full w-5 h-5 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity shadow-sm"
|
|
171
|
+
>
|
|
172
|
+
<X className="w-3 h-3" />
|
|
173
|
+
</button>
|
|
174
|
+
</div>
|
|
175
|
+
))}
|
|
176
|
+
{uploading && <span className="text-xs text-muted-foreground animate-pulse">Uploading…</span>}
|
|
177
|
+
</div>
|
|
178
|
+
)}
|
|
179
|
+
|
|
180
|
+
<div className="flex items-center gap-2 bg-muted/50 rounded-xl border px-3 py-2">
|
|
181
|
+
<Button
|
|
182
|
+
variant="ghost"
|
|
183
|
+
size="icon"
|
|
184
|
+
className="h-9 w-9 sm:h-8 sm:w-8 shrink-0"
|
|
185
|
+
onClick={() => fileRef.current?.click()}
|
|
186
|
+
disabled={disabled}
|
|
187
|
+
title="Attach file or image"
|
|
188
|
+
>
|
|
189
|
+
<Paperclip className="w-4 h-4" />
|
|
190
|
+
</Button>
|
|
191
|
+
<input ref={fileRef} type="file" className="hidden" onChange={handleFileChange} />
|
|
192
|
+
|
|
193
|
+
<AutocompleteTextarea
|
|
194
|
+
ref={acRef}
|
|
195
|
+
value={text}
|
|
196
|
+
onChange={setTextAndSave}
|
|
197
|
+
onKeyDown={handleKeyDown}
|
|
198
|
+
onPaste={handlePaste}
|
|
199
|
+
skills={skills}
|
|
200
|
+
workspaceFiles={workspaceFiles}
|
|
201
|
+
placeholder={busy ? 'Steer or queue…' : 'Message Claude…'}
|
|
202
|
+
disabled={disabled}
|
|
203
|
+
rows={1}
|
|
204
|
+
className="w-full bg-transparent border-none outline-none resize-none text-sm py-1.5 placeholder:text-muted-foreground min-h-[28px] max-h-[200px]"
|
|
205
|
+
/>
|
|
206
|
+
|
|
207
|
+
{slots.inputAdornments?.(inputCtx)}
|
|
208
|
+
|
|
209
|
+
{busy ? (
|
|
210
|
+
<div className="flex items-center gap-2">
|
|
211
|
+
{text.trim() && (
|
|
212
|
+
<Button
|
|
213
|
+
size="icon"
|
|
214
|
+
className="h-9 w-9 sm:h-8 sm:w-8 shrink-0 rounded-lg"
|
|
215
|
+
onClick={() => submit('steer')}
|
|
216
|
+
title="Steer (inject into current turn)"
|
|
217
|
+
>
|
|
218
|
+
<SendHorizontal className="w-4 h-4" />
|
|
219
|
+
</Button>
|
|
220
|
+
)}
|
|
221
|
+
<Button
|
|
222
|
+
variant="destructive"
|
|
223
|
+
size="icon"
|
|
224
|
+
className="h-9 w-9 sm:h-8 sm:w-8 shrink-0 rounded-lg"
|
|
225
|
+
onClick={() => onCancel()}
|
|
226
|
+
title="Stop generation"
|
|
227
|
+
>
|
|
228
|
+
<Square className="w-3.5 h-3.5" />
|
|
229
|
+
</Button>
|
|
230
|
+
</div>
|
|
231
|
+
) : (
|
|
232
|
+
<Button
|
|
233
|
+
size="icon"
|
|
234
|
+
className="h-9 w-9 sm:h-8 sm:w-8 shrink-0 rounded-lg"
|
|
235
|
+
onClick={() => submit()}
|
|
236
|
+
disabled={disabled || uploading || (!text.trim() && attachments.length === 0)}
|
|
237
|
+
>
|
|
238
|
+
<SendHorizontal className="w-4 h-4" />
|
|
239
|
+
</Button>
|
|
240
|
+
)}
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
{queue.length > 0 && (
|
|
244
|
+
<div className="flex gap-1.5 flex-wrap mt-2">
|
|
245
|
+
{queue.map((q, i) => (
|
|
246
|
+
<span key={i} className="inline-flex items-center gap-1 rounded-full bg-blue-50 dark:bg-blue-950/40 px-2.5 py-1 text-xs text-blue-700 dark:text-blue-300 ring-1 ring-inset ring-blue-600/20 dark:ring-blue-400/30">
|
|
247
|
+
<span className="truncate max-w-[200px]">{q}</span>
|
|
248
|
+
<button onClick={() => onRemoveQueued(i)} className="hover:text-blue-900 dark:hover:text-blue-100">
|
|
249
|
+
<X className="w-3 h-3" />
|
|
250
|
+
</button>
|
|
251
|
+
</span>
|
|
252
|
+
))}
|
|
253
|
+
</div>
|
|
254
|
+
)}
|
|
255
|
+
<p className="text-[10px] text-muted-foreground text-center mt-2">
|
|
256
|
+
{busy
|
|
257
|
+
? 'Enter to steer (redirect) · Tab to queue (after done) · Shift+Enter for newline'
|
|
258
|
+
: 'Enter to send · Shift+Enter for newline · @ for skills · / for commands'}
|
|
259
|
+
</p>
|
|
260
|
+
</div>
|
|
261
|
+
</div>
|
|
262
|
+
);
|
|
263
|
+
});
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Bot, ShieldCheck } from 'lucide-react';
|
|
3
|
+
import { Button } from './ui/button';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* OAuth consent screen for the embedded MCP Authorization Server.
|
|
7
|
+
* Rendered when the SPA is loaded at /oauth/authorize (claude.ai's authorization redirect).
|
|
8
|
+
* The user is already authenticated by the app's auth layer (provider-agnostic) — here they
|
|
9
|
+
* just approve the connection, which mints an auth code server-side and redirects back.
|
|
10
|
+
*/
|
|
11
|
+
export function OAuthConsent({ getToken, userEmail }: { getToken: () => Promise<string | null>; userEmail: string }) {
|
|
12
|
+
const [busy, setBusy] = useState(false);
|
|
13
|
+
const [error, setError] = useState('');
|
|
14
|
+
|
|
15
|
+
const params = new URLSearchParams(window.location.search);
|
|
16
|
+
const clientId = params.get('client_id') || '';
|
|
17
|
+
const redirectUri = params.get('redirect_uri') || '';
|
|
18
|
+
const state = params.get('state') || '';
|
|
19
|
+
const codeChallenge = params.get('code_challenge') || '';
|
|
20
|
+
const codeChallengeMethod = params.get('code_challenge_method') || 'S256';
|
|
21
|
+
const resource = params.get('resource') || '';
|
|
22
|
+
const clientName = params.get('client_name') || 'An MCP client';
|
|
23
|
+
|
|
24
|
+
const missing = !clientId || !redirectUri || !codeChallenge;
|
|
25
|
+
|
|
26
|
+
async function approve() {
|
|
27
|
+
setBusy(true);
|
|
28
|
+
setError('');
|
|
29
|
+
try {
|
|
30
|
+
const token = await getToken();
|
|
31
|
+
if (!token) throw new Error('Not signed in');
|
|
32
|
+
const res = await fetch('/oauth/authorize/consent', {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
35
|
+
body: JSON.stringify({
|
|
36
|
+
client_id: clientId,
|
|
37
|
+
redirect_uri: redirectUri,
|
|
38
|
+
code_challenge: codeChallenge,
|
|
39
|
+
code_challenge_method: codeChallengeMethod,
|
|
40
|
+
resource,
|
|
41
|
+
}),
|
|
42
|
+
});
|
|
43
|
+
if (!res.ok) {
|
|
44
|
+
const data = await res.json().catch(() => ({}));
|
|
45
|
+
throw new Error(data.error_description || data.error || `Request failed (${res.status})`);
|
|
46
|
+
}
|
|
47
|
+
const { code } = await res.json();
|
|
48
|
+
const url = new URL(redirectUri);
|
|
49
|
+
url.searchParams.set('code', code);
|
|
50
|
+
if (state) url.searchParams.set('state', state);
|
|
51
|
+
window.location.href = url.toString();
|
|
52
|
+
} catch (e: any) {
|
|
53
|
+
setError(e.message || 'Authorization failed');
|
|
54
|
+
setBusy(false);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function deny() {
|
|
59
|
+
if (!redirectUri) return;
|
|
60
|
+
try {
|
|
61
|
+
const url = new URL(redirectUri);
|
|
62
|
+
url.searchParams.set('error', 'access_denied');
|
|
63
|
+
if (state) url.searchParams.set('state', state);
|
|
64
|
+
window.location.href = url.toString();
|
|
65
|
+
} catch { /* invalid redirect — nothing to navigate to */ }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<div className="flex h-full items-center justify-center bg-background">
|
|
70
|
+
<div className="flex flex-col items-center gap-5 p-8 rounded-xl border bg-card shadow-sm w-full max-w-sm">
|
|
71
|
+
<div className="flex items-center gap-2">
|
|
72
|
+
<div className="flex items-center justify-center w-11 h-11 rounded-full bg-primary text-primary-foreground">
|
|
73
|
+
<Bot className="w-5 h-5" />
|
|
74
|
+
</div>
|
|
75
|
+
<ShieldCheck className="w-5 h-5 text-muted-foreground" />
|
|
76
|
+
</div>
|
|
77
|
+
<div className="text-center">
|
|
78
|
+
<h1 className="text-lg font-semibold">Authorize connection</h1>
|
|
79
|
+
<p className="text-sm text-muted-foreground mt-1">
|
|
80
|
+
<span className="font-medium text-foreground">{clientName}</span> wants to connect to your Shraga agent and
|
|
81
|
+
access it on your behalf.
|
|
82
|
+
</p>
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<p className="text-xs text-muted-foreground">Signed in as {userEmail}</p>
|
|
86
|
+
|
|
87
|
+
{missing && (
|
|
88
|
+
<p className="text-sm text-destructive text-center">Invalid authorization request — missing parameters.</p>
|
|
89
|
+
)}
|
|
90
|
+
{error && <p className="text-sm text-destructive text-center">{error}</p>}
|
|
91
|
+
|
|
92
|
+
<div className="flex gap-2 w-full">
|
|
93
|
+
<Button variant="outline" className="flex-1" onClick={deny} disabled={busy}>
|
|
94
|
+
Deny
|
|
95
|
+
</Button>
|
|
96
|
+
<Button className="flex-1" onClick={approve} disabled={busy || missing}>
|
|
97
|
+
{busy ? 'Authorizing…' : 'Approve'}
|
|
98
|
+
</Button>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { useState, useImperativeHandle, forwardRef } from 'react';
|
|
2
|
+
import { Clock } from 'lucide-react';
|
|
3
|
+
import { Button } from './ui/button';
|
|
4
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from './ui/dialog';
|
|
5
|
+
import { useSchedules } from '@/hooks/useSchedules';
|
|
6
|
+
import { useAuth } from '@/hooks/useAuth';
|
|
7
|
+
import { ScheduleList } from './schedules/ScheduleList';
|
|
8
|
+
import { ScheduleEditor } from './schedules/ScheduleEditor';
|
|
9
|
+
import type { Schedule } from '@/lib/schedule-types';
|
|
10
|
+
|
|
11
|
+
export interface SchedulesManagerHandle {
|
|
12
|
+
open: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Props {
|
|
16
|
+
getToken: () => Promise<string | null>;
|
|
17
|
+
onOpenSession: (sessionId: string) => void;
|
|
18
|
+
refreshKey?: number;
|
|
19
|
+
runningIds?: Set<string>;
|
|
20
|
+
skills?: string[];
|
|
21
|
+
workspaceFiles?: string[];
|
|
22
|
+
trigger?: React.ReactNode;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const SchedulesManager = forwardRef<SchedulesManagerHandle, Props>(function SchedulesManager({ getToken, onOpenSession, refreshKey = 0, runningIds = new Set(), skills = [], workspaceFiles = [], trigger }, ref) {
|
|
26
|
+
const [open, setOpen] = useState(false);
|
|
27
|
+
const { user } = useAuth();
|
|
28
|
+
const { schedules, serverRunningIds, create, update, remove, toggle, runNow, cancelRun } = useSchedules(getToken, open, refreshKey);
|
|
29
|
+
|
|
30
|
+
useImperativeHandle(ref, () => ({ open: () => setOpen(true) }), []);
|
|
31
|
+
const [editing, setEditing] = useState<Schedule | null>(null);
|
|
32
|
+
const [creating, setCreating] = useState(false);
|
|
33
|
+
const [pendingRunIds, setPendingRunIds] = useState<Set<string>>(new Set());
|
|
34
|
+
|
|
35
|
+
const closeEditor = () => { setEditing(null); setCreating(false); };
|
|
36
|
+
const handleRunNow = async (id: string, override?: string) => {
|
|
37
|
+
setPendingRunIds((prev) => new Set(prev).add(id));
|
|
38
|
+
try {
|
|
39
|
+
return await runNow(id, override);
|
|
40
|
+
} finally {
|
|
41
|
+
setTimeout(() => {
|
|
42
|
+
setPendingRunIds((prev) => {
|
|
43
|
+
const next = new Set(prev);
|
|
44
|
+
next.delete(id);
|
|
45
|
+
return next;
|
|
46
|
+
});
|
|
47
|
+
}, 1500);
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
return (
|
|
52
|
+
<Dialog open={open} onOpenChange={(o) => { setOpen(o); if (!o) closeEditor(); }}>
|
|
53
|
+
<DialogTrigger asChild>
|
|
54
|
+
{trigger || (
|
|
55
|
+
<Button variant="ghost" size="icon" className="h-8 w-8" title="Scheduled Tasks">
|
|
56
|
+
<Clock className="w-4 h-4" />
|
|
57
|
+
</Button>
|
|
58
|
+
)}
|
|
59
|
+
</DialogTrigger>
|
|
60
|
+
<DialogContent className="max-w-[95vw] sm:max-w-2xl">
|
|
61
|
+
<DialogHeader>
|
|
62
|
+
<DialogTitle>{creating ? 'New Schedule' : editing ? `Edit: ${editing.name}` : 'Scheduled Tasks'}</DialogTitle>
|
|
63
|
+
</DialogHeader>
|
|
64
|
+
|
|
65
|
+
{creating ? (
|
|
66
|
+
<ScheduleEditor
|
|
67
|
+
key="__new__"
|
|
68
|
+
onSave={async (s) => { await create(s); closeEditor(); }}
|
|
69
|
+
onCancel={closeEditor}
|
|
70
|
+
skills={skills}
|
|
71
|
+
workspaceFiles={workspaceFiles}
|
|
72
|
+
/>
|
|
73
|
+
) : editing ? (
|
|
74
|
+
<ScheduleEditor
|
|
75
|
+
key={editing.id}
|
|
76
|
+
initial={editing}
|
|
77
|
+
onSave={async (s) => { await update(editing.id, s); closeEditor(); }}
|
|
78
|
+
onCancel={closeEditor}
|
|
79
|
+
skills={skills}
|
|
80
|
+
workspaceFiles={workspaceFiles}
|
|
81
|
+
/>
|
|
82
|
+
) : (
|
|
83
|
+
<ScheduleList
|
|
84
|
+
schedules={schedules}
|
|
85
|
+
currentUserUid={user?.uid}
|
|
86
|
+
onCreate={() => setCreating(true)}
|
|
87
|
+
onEdit={(s) => setEditing(s)}
|
|
88
|
+
onDelete={(id) => { if (confirm('Delete this schedule?')) remove(id); }}
|
|
89
|
+
onToggle={toggle}
|
|
90
|
+
onRunNow={handleRunNow}
|
|
91
|
+
onCancel={cancelRun}
|
|
92
|
+
runningIds={new Set([...runningIds, ...serverRunningIds, ...pendingRunIds])}
|
|
93
|
+
onOpenRun={(sid) => { setOpen(false); onOpenSession(sid); }}
|
|
94
|
+
/>
|
|
95
|
+
)}
|
|
96
|
+
</DialogContent>
|
|
97
|
+
</Dialog>
|
|
98
|
+
);
|
|
99
|
+
});
|