shraga 0.0.2 → 0.1.1
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 -23
- 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 +60 -15
- 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,98 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Terminal, ShieldCheck } from 'lucide-react';
|
|
3
|
+
import { Button } from './ui/button';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* CLI auth consent — rendered when the SPA is loaded at /cli-auth (opened by `shraga term`/`shraga
|
|
7
|
+
* login`). The user is already authenticated by the app's auth layer; here they approve a terminal
|
|
8
|
+
* on a specific machine. Approve mints a scoped API key (owned by THIS user) via the normal
|
|
9
|
+
* /api/api-keys endpoint and hands it back to the CLI's loopback listener — so CLI-spawned terminals
|
|
10
|
+
* appear in the web UI under the user's own identity. No secret is ever shown or copied by hand.
|
|
11
|
+
*/
|
|
12
|
+
export function CliAuthConsent({ getToken, userEmail }: { getToken: () => Promise<string | null>; userEmail: string }) {
|
|
13
|
+
const [busy, setBusy] = useState(false);
|
|
14
|
+
const [done, setDone] = useState(false);
|
|
15
|
+
const [error, setError] = useState('');
|
|
16
|
+
|
|
17
|
+
const params = new URLSearchParams(window.location.search);
|
|
18
|
+
const port = params.get('port') || '';
|
|
19
|
+
const state = params.get('state') || '';
|
|
20
|
+
const host = params.get('host') || 'this machine';
|
|
21
|
+
const missing = !port || !state;
|
|
22
|
+
|
|
23
|
+
async function approve() {
|
|
24
|
+
setBusy(true);
|
|
25
|
+
setError('');
|
|
26
|
+
try {
|
|
27
|
+
const token = await getToken();
|
|
28
|
+
if (!token) throw new Error('Not signed in');
|
|
29
|
+
// Mint a key under the signed-in user (existing endpoint) — this is what ties CLI terminals to you.
|
|
30
|
+
const res = await fetch('/api/api-keys', {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: { 'Content-Type': 'application/json', Authorization: `Bearer ${token}` },
|
|
33
|
+
body: JSON.stringify({ label: `shraga term @ ${host}` }),
|
|
34
|
+
});
|
|
35
|
+
if (!res.ok) {
|
|
36
|
+
const data = await res.json().catch(() => ({}));
|
|
37
|
+
throw new Error(data.error || `Request failed (${res.status})`);
|
|
38
|
+
}
|
|
39
|
+
const { key } = (await res.json()) as { key: string };
|
|
40
|
+
// Hand the key to the CLI's loopback listener (127.0.0.1:<port>). CORS-preflighted POST.
|
|
41
|
+
const cb = await fetch(`http://127.0.0.1:${port}/token`, {
|
|
42
|
+
method: 'POST',
|
|
43
|
+
headers: { 'Content-Type': 'application/json' },
|
|
44
|
+
body: JSON.stringify({ token: key, state }),
|
|
45
|
+
});
|
|
46
|
+
if (!cb.ok) throw new Error('Could not reach the CLI on this machine — is `shraga term` still waiting?');
|
|
47
|
+
setDone(true);
|
|
48
|
+
} catch (e: any) {
|
|
49
|
+
setError(e.message || 'Authorization failed');
|
|
50
|
+
} finally {
|
|
51
|
+
setBusy(false);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className="flex h-full items-center justify-center bg-background">
|
|
57
|
+
<div className="flex flex-col items-center gap-5 p-8 rounded-xl border bg-card shadow-sm w-full max-w-sm text-center">
|
|
58
|
+
<div className="flex items-center gap-2">
|
|
59
|
+
<div className="flex items-center justify-center w-11 h-11 rounded-full bg-primary text-primary-foreground">
|
|
60
|
+
<Terminal className="w-5 h-5" />
|
|
61
|
+
</div>
|
|
62
|
+
<ShieldCheck className="w-5 h-5 text-muted-foreground" />
|
|
63
|
+
</div>
|
|
64
|
+
|
|
65
|
+
{done ? (
|
|
66
|
+
<>
|
|
67
|
+
<h1 className="text-lg font-semibold">Terminal authorized</h1>
|
|
68
|
+
<p className="text-sm text-muted-foreground">
|
|
69
|
+
Your terminal on <span className="font-medium text-foreground">{host}</span> is connected. You can close
|
|
70
|
+
this tab and return to it.
|
|
71
|
+
</p>
|
|
72
|
+
</>
|
|
73
|
+
) : (
|
|
74
|
+
<>
|
|
75
|
+
<div>
|
|
76
|
+
<h1 className="text-lg font-semibold">Authorize terminal</h1>
|
|
77
|
+
<p className="text-sm text-muted-foreground mt-1">
|
|
78
|
+
A terminal on <span className="font-medium text-foreground">{host}</span> wants to spawn and view shells
|
|
79
|
+
as you. Its terminals will show up here in the web UI.
|
|
80
|
+
</p>
|
|
81
|
+
</div>
|
|
82
|
+
<p className="text-xs text-muted-foreground">Signed in as {userEmail}</p>
|
|
83
|
+
{missing && <p className="text-sm text-destructive">Invalid request — missing parameters.</p>}
|
|
84
|
+
{error && <p className="text-sm text-destructive">{error}</p>}
|
|
85
|
+
<div className="flex gap-2 w-full">
|
|
86
|
+
<Button variant="outline" className="flex-1" onClick={() => window.close()} disabled={busy}>
|
|
87
|
+
Cancel
|
|
88
|
+
</Button>
|
|
89
|
+
<Button className="flex-1" onClick={approve} disabled={busy || missing}>
|
|
90
|
+
{busy ? 'Authorizing…' : 'Authorize'}
|
|
91
|
+
</Button>
|
|
92
|
+
</div>
|
|
93
|
+
</>
|
|
94
|
+
)}
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
);
|
|
98
|
+
}
|
|
@@ -0,0 +1,328 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { SlidersHorizontal } from 'lucide-react';
|
|
3
|
+
import { Button } from './ui/button';
|
|
4
|
+
import { Input } from './ui/input';
|
|
5
|
+
import { Textarea } from './ui/textarea';
|
|
6
|
+
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger, DialogBody, DialogFooter } from './ui/dialog';
|
|
7
|
+
import { useSlots } from '@/lib/slots';
|
|
8
|
+
|
|
9
|
+
interface AgentConfig {
|
|
10
|
+
model?: string;
|
|
11
|
+
engine?: string;
|
|
12
|
+
systemPrompt?: string;
|
|
13
|
+
allowedTools?: string[];
|
|
14
|
+
permissionMode?: string;
|
|
15
|
+
maxTurns?: number;
|
|
16
|
+
skillDiscovery?: boolean;
|
|
17
|
+
thinking?: 'adaptive' | 'enabled' | 'disabled';
|
|
18
|
+
effort?: 'low' | 'medium' | 'high' | 'max';
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface EngineModel {
|
|
22
|
+
value: string;
|
|
23
|
+
label: string;
|
|
24
|
+
provider?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface EngineInfo {
|
|
28
|
+
name: string;
|
|
29
|
+
models: EngineModel[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const FALLBACK_MODELS: EngineModel[] = [
|
|
33
|
+
{ value: '', label: 'Default (claude-sonnet-4-6)' },
|
|
34
|
+
{ value: 'claude-fable-5', label: 'Fable 5 — frontier, most capable' },
|
|
35
|
+
{ value: 'claude-opus-4-8', label: 'Opus 4.8 — most capable, best for complex/agentic tasks' },
|
|
36
|
+
{ value: 'claude-opus-4-7', label: 'Opus 4.7' },
|
|
37
|
+
{ value: 'claude-opus-4-6', label: 'Opus 4.6' },
|
|
38
|
+
{ value: 'claude-sonnet-4-6', label: 'Sonnet 4.6 — balanced speed & intelligence' },
|
|
39
|
+
{ value: 'claude-haiku-4-5', label: 'Haiku 4.5 — fastest' },
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
const PERMISSION_MODES = [
|
|
43
|
+
{ value: 'acceptEdits', label: 'Accept Edits (auto-approve file changes)' },
|
|
44
|
+
{ value: 'plan', label: 'Plan (read-only, suggest changes)' },
|
|
45
|
+
{ value: 'bypassPermissions', label: 'Bypass All (dangerous — no prompts)' },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
interface SessionDirectives { engine?: string; model?: string; turns?: number; thinking?: string }
|
|
49
|
+
|
|
50
|
+
interface Props {
|
|
51
|
+
getToken: () => Promise<string | null>;
|
|
52
|
+
onSaved?: (config: AgentConfig) => void;
|
|
53
|
+
trigger?: React.ReactNode;
|
|
54
|
+
/** When set, the runtime knobs (engine/model/turns/thinking) edit THIS session's directives. */
|
|
55
|
+
sessionId?: string;
|
|
56
|
+
sessionDirectives?: SessionDirectives;
|
|
57
|
+
onDirectivesSaved?: (directives: SessionDirectives) => void;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function ConfigPanel({ getToken, onSaved, trigger, sessionId, sessionDirectives, onDirectivesSaved }: Props) {
|
|
61
|
+
const slots = useSlots();
|
|
62
|
+
const [config, setConfig] = useState<AgentConfig>({});
|
|
63
|
+
const [open, setOpen] = useState(false);
|
|
64
|
+
const [saving, setSaving] = useState(false);
|
|
65
|
+
const [engines, setEngines] = useState<EngineInfo[]>([]);
|
|
66
|
+
const [multiEngine, setMultiEngine] = useState(false);
|
|
67
|
+
// Global config as loaded — so a per-session runtime change doesn't clobber the global defaults.
|
|
68
|
+
const globalRef = useRef<AgentConfig>({});
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (!open) return;
|
|
72
|
+
getToken().then((token) => {
|
|
73
|
+
if (!token) return;
|
|
74
|
+
fetch('/api/config', { headers: { Authorization: `Bearer ${token}` } })
|
|
75
|
+
.then((r) => r.json())
|
|
76
|
+
.then((global: AgentConfig) => {
|
|
77
|
+
globalRef.current = global;
|
|
78
|
+
// For an active session, the runtime knobs reflect the session's own directives
|
|
79
|
+
// (falling back to the global default when the session hasn't overridden them).
|
|
80
|
+
const sd = sessionId ? sessionDirectives : undefined;
|
|
81
|
+
setConfig({
|
|
82
|
+
...global,
|
|
83
|
+
...(sd?.engine !== undefined ? { engine: sd.engine } : {}),
|
|
84
|
+
...(sd?.model !== undefined ? { model: sd.model } : {}),
|
|
85
|
+
...(sd?.turns !== undefined ? { maxTurns: sd.turns } : {}),
|
|
86
|
+
...(sd?.thinking !== undefined ? { thinking: sd.thinking as AgentConfig['thinking'] } : {}),
|
|
87
|
+
});
|
|
88
|
+
})
|
|
89
|
+
.catch(() => {});
|
|
90
|
+
fetch('/api/engines', { headers: { Authorization: `Bearer ${token}` } })
|
|
91
|
+
.then((r) => r.json())
|
|
92
|
+
.then((data) => {
|
|
93
|
+
setEngines(data.engines ?? []);
|
|
94
|
+
setMultiEngine(data.multiEngine ?? false);
|
|
95
|
+
})
|
|
96
|
+
.catch(() => {});
|
|
97
|
+
});
|
|
98
|
+
}, [open, getToken, sessionId, sessionDirectives]);
|
|
99
|
+
|
|
100
|
+
const save = async () => {
|
|
101
|
+
setSaving(true);
|
|
102
|
+
const token = await getToken();
|
|
103
|
+
const auth = { Authorization: `Bearer ${token ?? ''}`, 'Content-Type': 'application/json' };
|
|
104
|
+
try {
|
|
105
|
+
if (sessionId) {
|
|
106
|
+
// Runtime knobs apply to THIS conversation (session directives shadow global at send time).
|
|
107
|
+
const runtime: SessionDirectives = {
|
|
108
|
+
engine: config.engine, model: config.model, turns: config.maxTurns, thinking: config.thinking,
|
|
109
|
+
};
|
|
110
|
+
const r = await fetch(`/api/sessions/${sessionId}/directives`, { method: 'PUT', headers: auth, body: JSON.stringify(runtime) })
|
|
111
|
+
.then((res) => res.json()).catch((e) => { console.warn('[config] directives save failed', e); return null; });
|
|
112
|
+
if (r?.directives) onDirectivesSaved?.(r.directives);
|
|
113
|
+
// Persist only the non-runtime fields globally — keep the global engine/model/turns/thinking intact.
|
|
114
|
+
const global: AgentConfig = {
|
|
115
|
+
...config,
|
|
116
|
+
engine: globalRef.current.engine, model: globalRef.current.model,
|
|
117
|
+
maxTurns: globalRef.current.maxTurns, thinking: globalRef.current.thinking,
|
|
118
|
+
};
|
|
119
|
+
await fetch('/api/config', { method: 'PUT', headers: auth, body: JSON.stringify(global) });
|
|
120
|
+
onSaved?.(global);
|
|
121
|
+
} else {
|
|
122
|
+
await fetch('/api/config', { method: 'PUT', headers: auth, body: JSON.stringify(config) });
|
|
123
|
+
onSaved?.(config);
|
|
124
|
+
}
|
|
125
|
+
setOpen(false);
|
|
126
|
+
} finally {
|
|
127
|
+
setSaving(false);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const currentEngine = config.engine || 'claude-code';
|
|
132
|
+
const engineInfo = engines.find(e => e.name === currentEngine);
|
|
133
|
+
const models = engineInfo?.models ?? FALLBACK_MODELS;
|
|
134
|
+
|
|
135
|
+
// A <select> whose value matches no option silently shows the first option while state stays
|
|
136
|
+
// unchanged — so switching to an engine without a "" Default entry (e.g. cursor) would persist
|
|
137
|
+
// model: undefined and the header pill would fall back to the global default. Snap to the first
|
|
138
|
+
// valid model when the current one isn't offered by the selected engine.
|
|
139
|
+
useEffect(() => {
|
|
140
|
+
if (!engineInfo || !models.length) return;
|
|
141
|
+
if (!models.some(m => (m.value || '') === (config.model ?? ''))) {
|
|
142
|
+
setConfig((c) => ({ ...c, model: models[0].value || undefined }));
|
|
143
|
+
}
|
|
144
|
+
}, [engineInfo, models, config.model]);
|
|
145
|
+
|
|
146
|
+
// Group models by provider if the engine has multi-provider models
|
|
147
|
+
const hasProviders = models.some(m => m.provider);
|
|
148
|
+
const groupedModels = hasProviders
|
|
149
|
+
? Array.from(
|
|
150
|
+
models.reduce((groups, m) => {
|
|
151
|
+
const key = m.provider || 'default';
|
|
152
|
+
if (!groups.has(key)) groups.set(key, []);
|
|
153
|
+
groups.get(key)!.push(m);
|
|
154
|
+
return groups;
|
|
155
|
+
}, new Map<string, EngineModel[]>())
|
|
156
|
+
)
|
|
157
|
+
: null;
|
|
158
|
+
|
|
159
|
+
const selectClass = 'flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2';
|
|
160
|
+
|
|
161
|
+
return (
|
|
162
|
+
<Dialog open={open} onOpenChange={setOpen}>
|
|
163
|
+
<DialogTrigger asChild>
|
|
164
|
+
{trigger || (
|
|
165
|
+
<Button variant="ghost" size="icon" title="Agent Config">
|
|
166
|
+
<SlidersHorizontal className="w-4 h-4" />
|
|
167
|
+
</Button>
|
|
168
|
+
)}
|
|
169
|
+
</DialogTrigger>
|
|
170
|
+
|
|
171
|
+
<DialogContent className="max-w-lg">
|
|
172
|
+
<DialogHeader>
|
|
173
|
+
<DialogTitle>Agent Configuration</DialogTitle>
|
|
174
|
+
</DialogHeader>
|
|
175
|
+
|
|
176
|
+
<DialogBody className="space-y-4">
|
|
177
|
+
{sessionId && (
|
|
178
|
+
<p className="text-xs text-muted-foreground rounded-md bg-muted/50 px-3 py-2">
|
|
179
|
+
Engine, Model, Max Turns & Thinking apply to <strong>this conversation</strong>. Other settings are global defaults for new sessions.
|
|
180
|
+
</p>
|
|
181
|
+
)}
|
|
182
|
+
{multiEngine && (
|
|
183
|
+
<div>
|
|
184
|
+
<label className="text-sm font-medium mb-1.5 block">Engine</label>
|
|
185
|
+
<select
|
|
186
|
+
value={config.engine ?? ''}
|
|
187
|
+
onChange={(e) => {
|
|
188
|
+
const engine = e.target.value || undefined;
|
|
189
|
+
setConfig((c) => ({ ...c, engine, model: undefined }));
|
|
190
|
+
}}
|
|
191
|
+
className={selectClass}
|
|
192
|
+
>
|
|
193
|
+
{engines.map((e) => (
|
|
194
|
+
<option key={e.name} value={e.name}>
|
|
195
|
+
{e.name === 'claude-code' ? 'Claude Code (default)' : e.name}
|
|
196
|
+
</option>
|
|
197
|
+
))}
|
|
198
|
+
</select>
|
|
199
|
+
</div>
|
|
200
|
+
)}
|
|
201
|
+
|
|
202
|
+
<div>
|
|
203
|
+
<label className="text-sm font-medium mb-1.5 block">Model</label>
|
|
204
|
+
<select
|
|
205
|
+
value={config.model ?? ''}
|
|
206
|
+
onChange={(e) => setConfig((c) => ({ ...c, model: e.target.value || undefined }))}
|
|
207
|
+
className={selectClass}
|
|
208
|
+
>
|
|
209
|
+
{groupedModels ? (
|
|
210
|
+
groupedModels.map(([provider, providerModels]) => (
|
|
211
|
+
<optgroup key={provider} label={provider.charAt(0).toUpperCase() + provider.slice(1)}>
|
|
212
|
+
{providerModels.map((m) => (
|
|
213
|
+
<option key={m.value} value={m.value}>{m.label}</option>
|
|
214
|
+
))}
|
|
215
|
+
</optgroup>
|
|
216
|
+
))
|
|
217
|
+
) : (
|
|
218
|
+
models.map((m) => (
|
|
219
|
+
<option key={m.value} value={m.value}>{m.label}</option>
|
|
220
|
+
))
|
|
221
|
+
)}
|
|
222
|
+
</select>
|
|
223
|
+
</div>
|
|
224
|
+
|
|
225
|
+
{slots.settingsSections?.({ getToken, sessionId })}
|
|
226
|
+
|
|
227
|
+
<div>
|
|
228
|
+
<label className="text-sm font-medium mb-1.5 block">Permission Mode</label>
|
|
229
|
+
<select
|
|
230
|
+
value={config.permissionMode ?? 'acceptEdits'}
|
|
231
|
+
onChange={(e) => setConfig((c) => ({ ...c, permissionMode: e.target.value }))}
|
|
232
|
+
className={selectClass}
|
|
233
|
+
>
|
|
234
|
+
{PERMISSION_MODES.map((m) => (
|
|
235
|
+
<option key={m.value} value={m.value}>{m.label}</option>
|
|
236
|
+
))}
|
|
237
|
+
</select>
|
|
238
|
+
</div>
|
|
239
|
+
|
|
240
|
+
<div>
|
|
241
|
+
<label className="text-sm font-medium mb-1.5 block">Max Turns</label>
|
|
242
|
+
<Input
|
|
243
|
+
type="number"
|
|
244
|
+
min={1}
|
|
245
|
+
max={200}
|
|
246
|
+
value={config.maxTurns ?? 50}
|
|
247
|
+
onChange={(e) => setConfig((c) => ({ ...c, maxTurns: Number(e.target.value) || 50 }))}
|
|
248
|
+
/>
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
<div>
|
|
252
|
+
<label className="text-sm font-medium mb-1.5 block">Thinking</label>
|
|
253
|
+
<select
|
|
254
|
+
value={config.thinking ?? ''}
|
|
255
|
+
onChange={(e) => setConfig((c) => ({ ...c, thinking: (e.target.value || undefined) as AgentConfig['thinking'] }))}
|
|
256
|
+
className={selectClass}
|
|
257
|
+
>
|
|
258
|
+
<option value="">Default (off)</option>
|
|
259
|
+
<option value="adaptive">Adaptive (Claude decides)</option>
|
|
260
|
+
<option value="enabled">Enabled (always think)</option>
|
|
261
|
+
<option value="disabled">Disabled</option>
|
|
262
|
+
</select>
|
|
263
|
+
</div>
|
|
264
|
+
|
|
265
|
+
<div>
|
|
266
|
+
<label className="text-sm font-medium mb-1.5 block">Effort</label>
|
|
267
|
+
<select
|
|
268
|
+
value={config.effort ?? ''}
|
|
269
|
+
onChange={(e) => setConfig((c) => ({ ...c, effort: (e.target.value || undefined) as AgentConfig['effort'] }))}
|
|
270
|
+
className={selectClass}
|
|
271
|
+
>
|
|
272
|
+
<option value="">Default</option>
|
|
273
|
+
<option value="low">Low — fast</option>
|
|
274
|
+
<option value="medium">Medium</option>
|
|
275
|
+
<option value="high">High</option>
|
|
276
|
+
<option value="max">Max</option>
|
|
277
|
+
</select>
|
|
278
|
+
</div>
|
|
279
|
+
|
|
280
|
+
<div>
|
|
281
|
+
<label className="text-sm font-medium mb-1.5 block">Allowed Tools</label>
|
|
282
|
+
<Input
|
|
283
|
+
placeholder="Read, Edit, Bash, WebSearch, Glob, LS"
|
|
284
|
+
value={(config.allowedTools ?? []).join(', ')}
|
|
285
|
+
onChange={(e) => setConfig((c) => ({
|
|
286
|
+
...c,
|
|
287
|
+
allowedTools: e.target.value.split(',').map((s) => s.trim()).filter(Boolean),
|
|
288
|
+
}))}
|
|
289
|
+
/>
|
|
290
|
+
<p className="text-xs text-muted-foreground mt-1">Comma-separated list of tools Claude can use</p>
|
|
291
|
+
</div>
|
|
292
|
+
|
|
293
|
+
<div className="flex items-center justify-between">
|
|
294
|
+
<div>
|
|
295
|
+
<label className="text-sm font-medium">Skill Discovery</label>
|
|
296
|
+
<p className="text-xs text-muted-foreground">Index all skills + auto-inject on trigger match</p>
|
|
297
|
+
</div>
|
|
298
|
+
<button
|
|
299
|
+
type="button"
|
|
300
|
+
role="switch"
|
|
301
|
+
aria-checked={config.skillDiscovery !== false}
|
|
302
|
+
onClick={() => setConfig((c) => ({ ...c, skillDiscovery: c.skillDiscovery === false ? true : false }))}
|
|
303
|
+
className={`relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors ${config.skillDiscovery !== false ? 'bg-primary' : 'bg-muted'}`}
|
|
304
|
+
>
|
|
305
|
+
<span className={`pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform ${config.skillDiscovery !== false ? 'translate-x-5' : 'translate-x-0'}`} />
|
|
306
|
+
</button>
|
|
307
|
+
</div>
|
|
308
|
+
|
|
309
|
+
<div>
|
|
310
|
+
<label className="text-sm font-medium mb-1.5 block">System Prompt (optional)</label>
|
|
311
|
+
<Textarea
|
|
312
|
+
placeholder="Additional instructions appended to Claude's system prompt…"
|
|
313
|
+
value={config.systemPrompt ?? ''}
|
|
314
|
+
onChange={(e) => setConfig((c) => ({ ...c, systemPrompt: e.target.value || undefined }))}
|
|
315
|
+
rows={4}
|
|
316
|
+
/>
|
|
317
|
+
</div>
|
|
318
|
+
</DialogBody>
|
|
319
|
+
|
|
320
|
+
<DialogFooter>
|
|
321
|
+
<Button onClick={save} disabled={saving}>
|
|
322
|
+
{saving ? 'Saving…' : 'Save Configuration'}
|
|
323
|
+
</Button>
|
|
324
|
+
</DialogFooter>
|
|
325
|
+
</DialogContent>
|
|
326
|
+
</Dialog>
|
|
327
|
+
);
|
|
328
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { useState } from 'react';
|
|
2
|
+
import { Copy, Check, Clock, GitFork, Layers } from 'lucide-react';
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import { ConfigPanel } from '@/components/ConfigPanel';
|
|
5
|
+
import type { AgentConfig } from '@/lib/workspaceContext';
|
|
6
|
+
|
|
7
|
+
interface SessionDirectives {
|
|
8
|
+
model?: string;
|
|
9
|
+
turns?: number;
|
|
10
|
+
thinking?: string;
|
|
11
|
+
engine?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function InfoBadges({
|
|
15
|
+
sessionId,
|
|
16
|
+
config,
|
|
17
|
+
sessionDirectives,
|
|
18
|
+
actualModel,
|
|
19
|
+
scheduleId,
|
|
20
|
+
onScheduleClick,
|
|
21
|
+
}: {
|
|
22
|
+
sessionId?: string;
|
|
23
|
+
config: AgentConfig;
|
|
24
|
+
sessionDirectives?: SessionDirectives;
|
|
25
|
+
/** Model the engine actually resolved at runtime (session meta `lastModel`) — beats configured/requested. */
|
|
26
|
+
actualModel?: string;
|
|
27
|
+
scheduleId?: string;
|
|
28
|
+
onScheduleClick?: () => void;
|
|
29
|
+
}) {
|
|
30
|
+
const [copied, setCopied] = useState(false);
|
|
31
|
+
const engine = sessionDirectives?.engine || config.engine || 'claude-code';
|
|
32
|
+
// The native Claude Code engine records a bare `claude-*` id; a multi-provider add-on engine records
|
|
33
|
+
// a `provider/model` id. Trust the recorded model only when its shape matches the current engine
|
|
34
|
+
// family, or a session that has since switched engines would show the previous engine's model.
|
|
35
|
+
const recordedByNative = actualModel ? !actualModel.includes('/') : undefined;
|
|
36
|
+
const engineIsNative = engine === 'claude-code' || engine === 'cursor';
|
|
37
|
+
const trustedActual = recordedByNative !== undefined && recordedByNative === engineIsNative ? actualModel : undefined;
|
|
38
|
+
const rawModel =
|
|
39
|
+
trustedActual || sessionDirectives?.model || config.model || (engine === 'cursor' ? 'cursor/composer-2.5' : 'sonnet-4-6');
|
|
40
|
+
// A multi-provider add-on engine runs any provider's model through its own loop, so it must be
|
|
41
|
+
// distinguishable from a native runtime running the same model. Prefix such a model with the engine
|
|
42
|
+
// name; native engines (claude-code, cursor) show the model plainly. Engine name comes from data.
|
|
43
|
+
const model = !engineIsNative ? `${engine} · ${rawModel.replace('claude-', '')}` : rawModel.replace('claude-', '');
|
|
44
|
+
const perms = config.permissionMode || 'acceptEdits';
|
|
45
|
+
const permLabel =
|
|
46
|
+
perms === 'bypassPermissions' ? 'bypass' : perms === 'plan' ? 'plan' : perms === 'default' ? 'prompt' : 'edits';
|
|
47
|
+
const thinking = (sessionDirectives?.thinking || config.thinking) as string | undefined;
|
|
48
|
+
|
|
49
|
+
const copySessionId = () => {
|
|
50
|
+
if (!sessionId) return;
|
|
51
|
+
navigator.clipboard.writeText(sessionId);
|
|
52
|
+
setCopied(true);
|
|
53
|
+
setTimeout(() => setCopied(false), 1500);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<div className="flex items-center gap-1.5 flex-wrap">
|
|
58
|
+
<span className="inline-flex items-center rounded-md bg-blue-50 px-1.5 py-0.5 text-[10px] font-medium text-blue-700 ring-1 ring-inset ring-blue-600/20 dark:bg-blue-950/50 dark:text-blue-300 dark:ring-blue-400/30">
|
|
59
|
+
{model}
|
|
60
|
+
</span>
|
|
61
|
+
<span className="inline-flex items-center rounded-md bg-amber-50 px-1.5 py-0.5 text-[10px] font-medium text-amber-700 ring-1 ring-inset ring-amber-600/20 dark:bg-amber-950/50 dark:text-amber-300 dark:ring-amber-400/30">
|
|
62
|
+
{permLabel}
|
|
63
|
+
</span>
|
|
64
|
+
<span className="inline-flex items-center rounded-md bg-green-50 px-1.5 py-0.5 text-[10px] font-medium text-green-700 ring-1 ring-inset ring-green-600/20 dark:bg-green-950/50 dark:text-green-300 dark:ring-green-400/30">
|
|
65
|
+
{sessionDirectives?.turns ?? config.maxTurns ?? 50} steps
|
|
66
|
+
</span>
|
|
67
|
+
{thinking && thinking !== 'disabled' && (
|
|
68
|
+
<span className="inline-flex items-center rounded-md bg-violet-50 px-1.5 py-0.5 text-[10px] font-medium text-violet-700 ring-1 ring-inset ring-violet-600/20 dark:bg-violet-950/50 dark:text-violet-300 dark:ring-violet-400/30">
|
|
69
|
+
think
|
|
70
|
+
</span>
|
|
71
|
+
)}
|
|
72
|
+
{sessionId && (
|
|
73
|
+
<button
|
|
74
|
+
onClick={copySessionId}
|
|
75
|
+
className="inline-flex items-center gap-0.5 rounded-md bg-muted px-1.5 py-0.5 text-[10px] font-mono text-muted-foreground ring-1 ring-inset ring-border hover:bg-accent transition-colors"
|
|
76
|
+
title={`Session: ${sessionId}\nClick to copy`}
|
|
77
|
+
>
|
|
78
|
+
{sessionId.slice(0, 8)}
|
|
79
|
+
{copied ? <Check className="w-2.5 h-2.5" /> : <Copy className="w-2.5 h-2.5" />}
|
|
80
|
+
</button>
|
|
81
|
+
)}
|
|
82
|
+
{scheduleId && (
|
|
83
|
+
<button
|
|
84
|
+
onClick={onScheduleClick}
|
|
85
|
+
className="inline-flex items-center gap-0.5 rounded-md bg-purple-50 px-1.5 py-0.5 text-[10px] font-mono text-purple-700 ring-1 ring-inset ring-purple-600/20 dark:bg-purple-950/50 dark:text-purple-300 dark:ring-purple-400/30 hover:bg-purple-100 dark:hover:bg-purple-950/70 transition-colors"
|
|
86
|
+
title="Open schedule details"
|
|
87
|
+
>
|
|
88
|
+
<Clock className="w-2.5 h-2.5" />
|
|
89
|
+
{scheduleId.slice(0, 8)}
|
|
90
|
+
</button>
|
|
91
|
+
)}
|
|
92
|
+
</div>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface ConversationHeaderProps {
|
|
97
|
+
sessionId?: string;
|
|
98
|
+
agentConfig: AgentConfig;
|
|
99
|
+
sessionDirectives?: SessionDirectives;
|
|
100
|
+
sessionLastModel?: string;
|
|
101
|
+
sessionScheduleId?: string;
|
|
102
|
+
artifactCount: number;
|
|
103
|
+
getToken: () => Promise<string | null>;
|
|
104
|
+
onConfigSaved: (c: AgentConfig) => void;
|
|
105
|
+
onDirectivesSaved: (d: SessionDirectives | undefined) => void;
|
|
106
|
+
onFork: () => void;
|
|
107
|
+
onToggleArtifacts: () => void;
|
|
108
|
+
onScheduleClick: () => void;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Per-conversation control strip (badges, fork, config, artifacts) rendered at the top of each pane. */
|
|
112
|
+
export function ConversationHeader({
|
|
113
|
+
sessionId,
|
|
114
|
+
agentConfig,
|
|
115
|
+
sessionDirectives,
|
|
116
|
+
sessionLastModel,
|
|
117
|
+
sessionScheduleId,
|
|
118
|
+
artifactCount,
|
|
119
|
+
getToken,
|
|
120
|
+
onConfigSaved,
|
|
121
|
+
onDirectivesSaved,
|
|
122
|
+
onFork,
|
|
123
|
+
onToggleArtifacts,
|
|
124
|
+
onScheduleClick,
|
|
125
|
+
}: ConversationHeaderProps) {
|
|
126
|
+
return (
|
|
127
|
+
<div className="flex items-center gap-1.5 px-2 sm:px-3 py-1.5 border-b bg-background/60 overflow-x-auto">
|
|
128
|
+
<InfoBadges
|
|
129
|
+
sessionId={sessionId}
|
|
130
|
+
config={agentConfig}
|
|
131
|
+
sessionDirectives={sessionDirectives}
|
|
132
|
+
actualModel={sessionLastModel}
|
|
133
|
+
scheduleId={sessionScheduleId}
|
|
134
|
+
onScheduleClick={onScheduleClick}
|
|
135
|
+
/>
|
|
136
|
+
<div className="flex-1" />
|
|
137
|
+
{sessionId && (
|
|
138
|
+
<Button variant="ghost" size="icon" className="h-7 w-7 shrink-0" onClick={onFork} title="Fork conversation">
|
|
139
|
+
<GitFork className="w-3.5 h-3.5" />
|
|
140
|
+
</Button>
|
|
141
|
+
)}
|
|
142
|
+
<ConfigPanel
|
|
143
|
+
getToken={getToken}
|
|
144
|
+
onSaved={onConfigSaved}
|
|
145
|
+
sessionId={sessionId}
|
|
146
|
+
sessionDirectives={sessionDirectives}
|
|
147
|
+
onDirectivesSaved={onDirectivesSaved}
|
|
148
|
+
/>
|
|
149
|
+
{artifactCount > 0 && (
|
|
150
|
+
<Button variant="ghost" size="icon" className="h-7 w-7 shrink-0" onClick={onToggleArtifacts} title="Toggle artifacts panel">
|
|
151
|
+
<Layers className="w-3.5 h-3.5" />
|
|
152
|
+
</Button>
|
|
153
|
+
)}
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
}
|