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.
Files changed (180) hide show
  1. package/README.md +82 -27
  2. package/defaults/agents/summarizer.md +16 -0
  3. package/defaults/agents/trace-extractor.md +84 -0
  4. package/defaults/bin/claude +45 -0
  5. package/defaults/bin/claude-revive +17 -0
  6. package/defaults/extensions/README.md +70 -0
  7. package/defaults/extensions/selftest.ext.ts +43 -0
  8. package/defaults/extensions/stripe-webhook.ext.ts +58 -0
  9. package/defaults/gmail-triage-prompt.md +42 -0
  10. package/defaults/scripts/README +4 -0
  11. package/defaults/scripts/agent-once.ts +67 -0
  12. package/defaults/scripts/backfill-slack-usernames.ts +82 -0
  13. package/defaults/scripts/notifier-throttle.ts +44 -0
  14. package/defaults/scripts/summarize-conversations.ts +5 -0
  15. package/defaults/shraga.config.ts +29 -0
  16. package/defaults/skills/add-skill.md +14 -0
  17. package/defaults/skills/artifacts.md +116 -0
  18. package/defaults/skills/code-review.md +26 -0
  19. package/defaults/skills/communications.md +54 -0
  20. package/defaults/skills/context-audit.md +87 -0
  21. package/defaults/skills/debug.md +10 -0
  22. package/defaults/skills/garden.md +179 -0
  23. package/defaults/skills/github-contributor.md +35 -0
  24. package/defaults/skills/identity.md +30 -0
  25. package/defaults/skills/mcp-server.md +62 -0
  26. package/defaults/skills/mcps-sync.md +105 -0
  27. package/defaults/skills/plan.md +9 -0
  28. package/defaults/skills/platform.md +177 -0
  29. package/defaults/skills/reconcile.md +239 -0
  30. package/defaults/skills/scheduler.md +192 -0
  31. package/defaults/skills/self-aware.md +136 -0
  32. package/defaults/skills/shraga-know.md +333 -0
  33. package/defaults/skills/stripe.md +55 -0
  34. package/defaults/skills/write-tests.md +10 -0
  35. package/defaults/skills-defaults.json +1 -0
  36. package/defaults/system-prompt.md +46 -0
  37. package/defaults/workspace/context.md +28 -0
  38. package/defaults/workspace.md +50 -0
  39. package/defaults/zdotdir/.gitignore +8 -0
  40. package/defaults/zdotdir/.zlogin +3 -0
  41. package/defaults/zdotdir/.zprofile +1 -0
  42. package/defaults/zdotdir/.zshenv +4 -0
  43. package/defaults/zdotdir/.zshrc +3 -0
  44. package/dist/client/assets/index-BoHttkMt.js +1940 -0
  45. package/dist/client/assets/index-DdibEb2O.css +10 -0
  46. package/dist/client/index.html +22 -0
  47. package/package.json +59 -14
  48. package/src/cli.ts +71 -46
  49. package/src/client/App.tsx +510 -0
  50. package/src/client/components/ArtifactCard.tsx +26 -0
  51. package/src/client/components/ArtifactPanel.tsx +138 -0
  52. package/src/client/components/AuthedImage.tsx +85 -0
  53. package/src/client/components/AutocompleteTextarea.tsx +149 -0
  54. package/src/client/components/ChatView.tsx +866 -0
  55. package/src/client/components/CliAuthConsent.tsx +98 -0
  56. package/src/client/components/ConfigPanel.tsx +328 -0
  57. package/src/client/components/ConversationHeader.tsx +156 -0
  58. package/src/client/components/ConversationPane.tsx +277 -0
  59. package/src/client/components/LoginPage.tsx +81 -0
  60. package/src/client/components/MachineStats.tsx +77 -0
  61. package/src/client/components/McpManager.tsx +209 -0
  62. package/src/client/components/MessageInput.tsx +263 -0
  63. package/src/client/components/OAuthConsent.tsx +103 -0
  64. package/src/client/components/SchedulesManager.tsx +99 -0
  65. package/src/client/components/Sidebar.tsx +235 -0
  66. package/src/client/components/SkillsManager.tsx +280 -0
  67. package/src/client/components/SmartChart.tsx +167 -0
  68. package/src/client/components/Toast.tsx +54 -0
  69. package/src/client/components/WorkspaceTree.tsx +313 -0
  70. package/src/client/components/ZoomableImage.tsx +123 -0
  71. package/src/client/components/artifact-presets.ts +10 -0
  72. package/src/client/components/schedules/ScheduleEditor.tsx +264 -0
  73. package/src/client/components/schedules/ScheduleList.tsx +271 -0
  74. package/src/client/components/ui/accordion.tsx +50 -0
  75. package/src/client/components/ui/button.tsx +43 -0
  76. package/src/client/components/ui/dialog.tsx +82 -0
  77. package/src/client/components/ui/input.tsx +19 -0
  78. package/src/client/components/ui/scroll-area.tsx +39 -0
  79. package/src/client/components/ui/textarea.tsx +18 -0
  80. package/src/client/globals.css +51 -0
  81. package/src/client/hooks/useAgentSocket.ts +79 -0
  82. package/src/client/hooks/useArtifacts.ts +89 -0
  83. package/src/client/hooks/useAuth.ts +127 -0
  84. package/src/client/hooks/useConversation.ts +412 -0
  85. package/src/client/hooks/useDarkMode.ts +57 -0
  86. package/src/client/hooks/useIsMobile.ts +23 -0
  87. package/src/client/hooks/usePush.ts +127 -0
  88. package/src/client/hooks/useSchedules.ts +73 -0
  89. package/src/client/hooks/useUnread.ts +238 -0
  90. package/src/client/lib/desktopAttention.ts +75 -0
  91. package/src/client/lib/firebase.ts +32 -0
  92. package/src/client/lib/googleAuthNative.ts +94 -0
  93. package/src/client/lib/native.ts +43 -0
  94. package/src/client/lib/schedule-types.ts +34 -0
  95. package/src/client/lib/sessionApi.ts +58 -0
  96. package/src/client/lib/slots.tsx +79 -0
  97. package/src/client/lib/storage.ts +39 -0
  98. package/src/client/lib/utils.ts +26 -0
  99. package/src/client/lib/workspaceContext.tsx +54 -0
  100. package/src/client/lib/ws.ts +203 -0
  101. package/src/client/main.tsx +14 -0
  102. package/src/mcp-stdio-bridge.ts +70 -0
  103. package/src/scripts/summarize-conversations.ts +5 -0
  104. package/src/scripts/typecheck.ts +43 -0
  105. package/src/server/agents.ts +54 -0
  106. package/src/server/api-keys.ts +63 -0
  107. package/src/server/artifacts/artifacts.export.ts +85 -0
  108. package/src/server/artifacts/artifacts.handler.ts +93 -0
  109. package/src/server/artifacts/artifacts.routes.ts +43 -0
  110. package/src/server/artifacts/artifacts.service.ts +100 -0
  111. package/src/server/artifacts/artifacts.types.ts +31 -0
  112. package/src/server/auth.ts +262 -0
  113. package/src/server/claude.ts +394 -0
  114. package/src/server/commands.ts +21 -0
  115. package/src/server/contacts.ts +177 -0
  116. package/src/server/conversation-summarizer.ts +204 -0
  117. package/src/server/data-sync.ts +664 -0
  118. package/src/server/directives.ts +91 -0
  119. package/src/server/engine/claude-code.ts +514 -0
  120. package/src/server/engine/index.ts +41 -0
  121. package/src/server/engine/registry.ts +21 -0
  122. package/src/server/engine/shared.ts +47 -0
  123. package/src/server/engine/types.ts +48 -0
  124. package/src/server/env-resolve.ts +71 -0
  125. package/src/server/env-sanitize.ts +9 -0
  126. package/src/server/events/bus.ts +29 -0
  127. package/src/server/events/dispatcher.ts +48 -0
  128. package/src/server/events/routes.ts +19 -0
  129. package/src/server/events/types.ts +9 -0
  130. package/src/server/extensions.ts +101 -0
  131. package/src/server/features.ts +109 -0
  132. package/src/server/file-inject.ts +45 -0
  133. package/src/server/hooks.ts +142 -0
  134. package/src/server/idempotency.ts +25 -0
  135. package/src/server/index.ts +1715 -0
  136. package/src/server/integrity-audit.ts +132 -0
  137. package/src/server/mcp-catalog.ts +70 -0
  138. package/src/server/mcp-oauth.ts +198 -0
  139. package/src/server/mcp-progress.ts +45 -0
  140. package/src/server/mcp-server.ts +456 -0
  141. package/src/server/mcp-sidecar.ts +87 -0
  142. package/src/server/mcp.ts +291 -0
  143. package/src/server/model-aliases.ts +76 -0
  144. package/src/server/paths.ts +24 -0
  145. package/src/server/polls.ts +175 -0
  146. package/src/server/push/apns.ts +113 -0
  147. package/src/server/push/fcm.ts +108 -0
  148. package/src/server/push/push.ts +66 -0
  149. package/src/server/push/store.ts +84 -0
  150. package/src/server/push/triggers.ts +99 -0
  151. package/src/server/scheduler/builtins.ts +157 -0
  152. package/src/server/scheduler/engine.ts +432 -0
  153. package/src/server/scheduler/index.ts +4 -0
  154. package/src/server/scheduler/runner.ts +334 -0
  155. package/src/server/scheduler/storage.ts +98 -0
  156. package/src/server/scheduler/timing.ts +70 -0
  157. package/src/server/scheduler/types.ts +62 -0
  158. package/src/server/sdk-utils.ts +45 -0
  159. package/src/server/seed.ts +174 -0
  160. package/src/server/session-bus.ts +18 -0
  161. package/src/server/sessions.ts +559 -0
  162. package/src/server/shraga-config.ts +167 -0
  163. package/src/server/skills.ts +372 -0
  164. package/src/server/slack/api.ts +37 -0
  165. package/src/server/slack/bot.ts +391 -0
  166. package/src/server/slack/context-cache.ts +42 -0
  167. package/src/server/slack/feature.ts +59 -0
  168. package/src/server/slack/mention-rewrite.ts +59 -0
  169. package/src/server/slack/oauth.ts +102 -0
  170. package/src/server/slack/questions.ts +112 -0
  171. package/src/server/slack/sessions.ts +139 -0
  172. package/src/server/stats.ts +106 -0
  173. package/src/server/summarize.ts +11 -0
  174. package/src/server/turn-context.ts +61 -0
  175. package/src/server/unclaw-config.ts +19 -0
  176. package/src/server/unread.ts +79 -0
  177. package/src/server/user-context.ts +33 -0
  178. package/src/server/vendor-sync.ts +52 -0
  179. package/src/server/voice-provider.ts +74 -0
  180. 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
+ });