-
Notifications
You must be signed in to change notification settings - Fork 0
Add sandbox and approval run profile controls #100
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,13 +1,14 @@ | ||||||||||||||||||||||||||||||||||||||||||||
| import { memo, useCallback, useRef, useState } from 'react' | ||||||||||||||||||||||||||||||||||||||||||||
| import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' | ||||||||||||||||||||||||||||||||||||||||||||
| import { HugeiconsIcon } from '@hugeicons/react' | ||||||||||||||||||||||||||||||||||||||||||||
| import { ArrowUp02Icon } from '@hugeicons/core-free-icons' | ||||||||||||||||||||||||||||||||||||||||||||
| import { fetchWorkspaces, updateWorkspace } from '../chat-queries' | ||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||
| RepoContextButton, | ||||||||||||||||||||||||||||||||||||||||||||
| RepoContextPanel, | ||||||||||||||||||||||||||||||||||||||||||||
| RepoContextSummary, | ||||||||||||||||||||||||||||||||||||||||||||
| } from './repo-context-picker' | ||||||||||||||||||||||||||||||||||||||||||||
| import type { Ref } from 'react' | ||||||||||||||||||||||||||||||||||||||||||||
| import type { RepoContextSelection } from '../types' | ||||||||||||||||||||||||||||||||||||||||||||
| import type { RepoContextSelection, RunProfileId } from '../types' | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| import type { AttachmentFile } from '@/components/attachment-button' | ||||||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -34,8 +35,34 @@ type ChatComposerHelpers = { | |||||||||||||||||||||||||||||||||||||||||||
| setValue: (value: string) => void | ||||||||||||||||||||||||||||||||||||||||||||
| attachments?: Array<AttachmentFile> | ||||||||||||||||||||||||||||||||||||||||||||
| contextSelections?: Array<RepoContextSelection> | ||||||||||||||||||||||||||||||||||||||||||||
| runProfile?: RunProfileId | ||||||||||||||||||||||||||||||||||||||||||||
| confirmedRisk?: boolean | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| const runProfiles = [ | ||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||
| id: 'read-only-inspect', | ||||||||||||||||||||||||||||||||||||||||||||
| label: 'Inspect', | ||||||||||||||||||||||||||||||||||||||||||||
| sandbox: 'read-only', | ||||||||||||||||||||||||||||||||||||||||||||
| approval: 'untrusted', | ||||||||||||||||||||||||||||||||||||||||||||
| requiresConfirmation: false, | ||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||
| id: 'workspace-write', | ||||||||||||||||||||||||||||||||||||||||||||
| label: 'Write', | ||||||||||||||||||||||||||||||||||||||||||||
| sandbox: 'workspace-write', | ||||||||||||||||||||||||||||||||||||||||||||
| approval: 'on-request', | ||||||||||||||||||||||||||||||||||||||||||||
| requiresConfirmation: true, | ||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||
| id: 'elevated-manual-review', | ||||||||||||||||||||||||||||||||||||||||||||
| label: 'Elevated', | ||||||||||||||||||||||||||||||||||||||||||||
| sandbox: 'danger-full-access', | ||||||||||||||||||||||||||||||||||||||||||||
| approval: 'untrusted', | ||||||||||||||||||||||||||||||||||||||||||||
| requiresConfirmation: true, | ||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||
| ] as const | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| function ChatComposerComponent({ | ||||||||||||||||||||||||||||||||||||||||||||
| onSubmit, | ||||||||||||||||||||||||||||||||||||||||||||
| isLoading, | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -47,6 +74,10 @@ function ChatComposerComponent({ | |||||||||||||||||||||||||||||||||||||||||||
| const [contextSelections, setContextSelections] = useState< | ||||||||||||||||||||||||||||||||||||||||||||
| Array<RepoContextSelection> | ||||||||||||||||||||||||||||||||||||||||||||
| >([]) | ||||||||||||||||||||||||||||||||||||||||||||
| const [runProfile, setRunProfile] = | ||||||||||||||||||||||||||||||||||||||||||||
| useState<RunProfileId>('read-only-inspect') | ||||||||||||||||||||||||||||||||||||||||||||
| const [confirmedRisk, setConfirmedRisk] = useState(false) | ||||||||||||||||||||||||||||||||||||||||||||
| const [activeWorkspaceId, setActiveWorkspaceId] = useState('') | ||||||||||||||||||||||||||||||||||||||||||||
| const promptRef = useRef<HTMLTextAreaElement | null>(null) | ||||||||||||||||||||||||||||||||||||||||||||
| const valueRef = useRef('') | ||||||||||||||||||||||||||||||||||||||||||||
| const setValueRef = useRef<((value: string) => void) | null>(null) | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -70,8 +101,29 @@ function ChatComposerComponent({ | |||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||
| setContextSelections([]) | ||||||||||||||||||||||||||||||||||||||||||||
| setContextOpen(false) | ||||||||||||||||||||||||||||||||||||||||||||
| setConfirmedRisk(false) | ||||||||||||||||||||||||||||||||||||||||||||
| focusPrompt() | ||||||||||||||||||||||||||||||||||||||||||||
| }, [focusPrompt]) | ||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||
| let cancelled = false | ||||||||||||||||||||||||||||||||||||||||||||
| fetchWorkspaces() | ||||||||||||||||||||||||||||||||||||||||||||
| .then((data) => { | ||||||||||||||||||||||||||||||||||||||||||||
| if (cancelled) return | ||||||||||||||||||||||||||||||||||||||||||||
| const activeWorkspace = data.workspaces.find( | ||||||||||||||||||||||||||||||||||||||||||||
| (workspace) => workspace.id === data.activeWorkspaceId, | ||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||
| setActiveWorkspaceId(data.activeWorkspaceId) | ||||||||||||||||||||||||||||||||||||||||||||
| if (activeWorkspace?.runProfile) { | ||||||||||||||||||||||||||||||||||||||||||||
| setRunProfile(activeWorkspace.runProfile) | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||
| .catch(() => { | ||||||||||||||||||||||||||||||||||||||||||||
| // ignore | ||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||
| return () => { | ||||||||||||||||||||||||||||||||||||||||||||
| cancelled = true | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
| }, []) | ||||||||||||||||||||||||||||||||||||||||||||
| const handleFileSelect = useCallback((file: AttachmentFile) => { | ||||||||||||||||||||||||||||||||||||||||||||
| setAttachments((prev) => [...prev, file]) | ||||||||||||||||||||||||||||||||||||||||||||
| }, []) | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -89,6 +141,28 @@ function ChatComposerComponent({ | |||||||||||||||||||||||||||||||||||||||||||
| prev.filter((selection) => selection.path !== path), | ||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||
| }, []) | ||||||||||||||||||||||||||||||||||||||||||||
| const activeRunProfile = useMemo(() => { | ||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||
| runProfiles.find((profile) => profile.id === runProfile) ?? runProfiles[0] | ||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||
| }, [runProfile]) | ||||||||||||||||||||||||||||||||||||||||||||
| const handleRunProfileChange = useCallback( | ||||||||||||||||||||||||||||||||||||||||||||
| (value: string) => { | ||||||||||||||||||||||||||||||||||||||||||||
| const nextProfile = | ||||||||||||||||||||||||||||||||||||||||||||
| runProfiles.find((profile) => profile.id === value) ?? runProfiles[0] | ||||||||||||||||||||||||||||||||||||||||||||
| setRunProfile(nextProfile.id) | ||||||||||||||||||||||||||||||||||||||||||||
| setConfirmedRisk(false) | ||||||||||||||||||||||||||||||||||||||||||||
| if (activeWorkspaceId) { | ||||||||||||||||||||||||||||||||||||||||||||
| void updateWorkspace({ | ||||||||||||||||||||||||||||||||||||||||||||
| id: activeWorkspaceId, | ||||||||||||||||||||||||||||||||||||||||||||
| runProfile: nextProfile.id, | ||||||||||||||||||||||||||||||||||||||||||||
| codexSandbox: nextProfile.sandbox, | ||||||||||||||||||||||||||||||||||||||||||||
| codexApproval: nextProfile.approval, | ||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+155
to
+162
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Run profile changes can be saved to a stale workspace id. Prompt for AI agents
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||
| }, | ||||||||||||||||||||||||||||||||||||||||||||
| [activeWorkspaceId], | ||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||
| const setComposerValue = useCallback( | ||||||||||||||||||||||||||||||||||||||||||||
| (nextValue: string) => { | ||||||||||||||||||||||||||||||||||||||||||||
| if (setValueRef.current) { | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -109,11 +183,14 @@ function ChatComposerComponent({ | |||||||||||||||||||||||||||||||||||||||||||
| contextSelections.length === 0 | ||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||
| return | ||||||||||||||||||||||||||||||||||||||||||||
| if (activeRunProfile.requiresConfirmation && !confirmedRisk) return | ||||||||||||||||||||||||||||||||||||||||||||
| onSubmit(body, { | ||||||||||||||||||||||||||||||||||||||||||||
| reset, | ||||||||||||||||||||||||||||||||||||||||||||
| setValue: setComposerValue, | ||||||||||||||||||||||||||||||||||||||||||||
| attachments: validAttachments, | ||||||||||||||||||||||||||||||||||||||||||||
| contextSelections, | ||||||||||||||||||||||||||||||||||||||||||||
| runProfile, | ||||||||||||||||||||||||||||||||||||||||||||
| confirmedRisk, | ||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||
| focusPrompt() | ||||||||||||||||||||||||||||||||||||||||||||
| }, [ | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -124,8 +201,12 @@ function ChatComposerComponent({ | |||||||||||||||||||||||||||||||||||||||||||
| setComposerValue, | ||||||||||||||||||||||||||||||||||||||||||||
| attachments, | ||||||||||||||||||||||||||||||||||||||||||||
| contextSelections, | ||||||||||||||||||||||||||||||||||||||||||||
| runProfile, | ||||||||||||||||||||||||||||||||||||||||||||
| confirmedRisk, | ||||||||||||||||||||||||||||||||||||||||||||
| activeRunProfile.requiresConfirmation, | ||||||||||||||||||||||||||||||||||||||||||||
| ]) | ||||||||||||||||||||||||||||||||||||||||||||
| const submitDisabled = disabled | ||||||||||||||||||||||||||||||||||||||||||||
| const submitDisabled = | ||||||||||||||||||||||||||||||||||||||||||||
| disabled || (activeRunProfile.requiresConfirmation && !confirmedRisk) | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||
| <div | ||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -157,6 +238,34 @@ function ChatComposerComponent({ | |||||||||||||||||||||||||||||||||||||||||||
| placeholder="Type a message…" | ||||||||||||||||||||||||||||||||||||||||||||
| inputRef={promptRef} | ||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex flex-wrap items-center gap-2 px-3 pb-2 text-xs text-primary-500"> | ||||||||||||||||||||||||||||||||||||||||||||
| <select | ||||||||||||||||||||||||||||||||||||||||||||
| value={runProfile} | ||||||||||||||||||||||||||||||||||||||||||||
| onChange={(event) => handleRunProfileChange(event.target.value)} | ||||||||||||||||||||||||||||||||||||||||||||
| className="h-7 rounded-md border border-primary-200 bg-surface px-2 text-xs text-primary-800 outline-none" | ||||||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||||||
| {runProfiles.map((profile) => ( | ||||||||||||||||||||||||||||||||||||||||||||
| <option key={profile.id} value={profile.id}> | ||||||||||||||||||||||||||||||||||||||||||||
| {profile.label} | ||||||||||||||||||||||||||||||||||||||||||||
| </option> | ||||||||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||||||||
| </select> | ||||||||||||||||||||||||||||||||||||||||||||
| <span> | ||||||||||||||||||||||||||||||||||||||||||||
| sandbox: {activeRunProfile.sandbox} · approval:{' '} | ||||||||||||||||||||||||||||||||||||||||||||
| {activeRunProfile.approval} | ||||||||||||||||||||||||||||||||||||||||||||
| </span> | ||||||||||||||||||||||||||||||||||||||||||||
| {activeRunProfile.requiresConfirmation ? ( | ||||||||||||||||||||||||||||||||||||||||||||
| <label className="inline-flex items-center gap-1 text-amber-700"> | ||||||||||||||||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||||||||||||||||
| type="checkbox" | ||||||||||||||||||||||||||||||||||||||||||||
| checked={confirmedRisk} | ||||||||||||||||||||||||||||||||||||||||||||
| onChange={(event) => setConfirmedRisk(event.target.checked)} | ||||||||||||||||||||||||||||||||||||||||||||
| className="size-3.5" | ||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||
| confirm | ||||||||||||||||||||||||||||||||||||||||||||
| </label> | ||||||||||||||||||||||||||||||||||||||||||||
| ) : null} | ||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||
| <PromptInputActions className="justify-end px-3"> | ||||||||||||||||||||||||||||||||||||||||||||
| <div className="flex items-center gap-2 min-h-8 flex-nowrap"> | ||||||||||||||||||||||||||||||||||||||||||||
| <PromptInputAction | ||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -38,6 +38,8 @@ type WorkspaceDraft = Partial< | |||||
| | 'name' | ||||||
| | 'codexCommand' | ||||||
| | 'codexSandbox' | ||||||
| | 'codexApproval' | ||||||
| | 'runProfile' | ||||||
| | 'codexWorkdir' | ||||||
| | 'stateDir' | ||||||
| > | ||||||
|
|
@@ -97,6 +99,27 @@ function WorkspaceField({ label, children }: WorkspaceFieldProps) { | |||||
|
|
||||||
| const newWorkspaceId = '__new__' | ||||||
| const sandboxOptions = ['read-only', 'workspace-write', 'danger-full-access'] | ||||||
| const approvalOptions = ['untrusted', 'on-request', 'never'] | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Approval dropdown is missing the supported Prompt for AI agents
Suggested change
|
||||||
| const runProfileOptions = [ | ||||||
| { | ||||||
| id: 'read-only-inspect', | ||||||
| label: 'Read-only inspect', | ||||||
| sandbox: 'read-only', | ||||||
| approval: 'untrusted', | ||||||
| }, | ||||||
| { | ||||||
| id: 'workspace-write', | ||||||
| label: 'Workspace write', | ||||||
| sandbox: 'workspace-write', | ||||||
| approval: 'on-request', | ||||||
| }, | ||||||
| { | ||||||
| id: 'elevated-manual-review', | ||||||
| label: 'Elevated manual review', | ||||||
| sandbox: 'danger-full-access', | ||||||
| approval: 'untrusted', | ||||||
| }, | ||||||
| ] as const | ||||||
|
|
||||||
| function emptyWorkspaceDraft( | ||||||
| workspace?: WorkspaceSummary | null, | ||||||
|
|
@@ -105,6 +128,8 @@ function emptyWorkspaceDraft( | |||||
| name: workspace ? `${workspace.name} copy` : 'New workspace', | ||||||
| codexCommand: workspace?.codexCommand ?? 'codex', | ||||||
| codexSandbox: workspace?.codexSandbox ?? 'read-only', | ||||||
| codexApproval: workspace?.codexApproval ?? 'untrusted', | ||||||
| runProfile: workspace?.runProfile ?? 'read-only-inspect', | ||||||
| codexWorkdir: workspace?.codexWorkdir ?? '', | ||||||
| stateDir: workspace?.stateDir ?? '', | ||||||
| } | ||||||
|
|
@@ -210,6 +235,8 @@ export function SettingsDialog({ | |||||
| name: selectedWorkspace.name, | ||||||
| codexCommand: selectedWorkspace.codexCommand, | ||||||
| codexSandbox: selectedWorkspace.codexSandbox, | ||||||
| codexApproval: selectedWorkspace.codexApproval, | ||||||
| runProfile: selectedWorkspace.runProfile, | ||||||
| codexWorkdir: selectedWorkspace.codexWorkdir, | ||||||
| stateDir: selectedWorkspace.stateDir, | ||||||
| }) | ||||||
|
|
@@ -219,6 +246,16 @@ export function SettingsDialog({ | |||||
| setDraft((current) => ({ ...current, [field]: value })) | ||||||
| } | ||||||
|
|
||||||
| function updateRunProfile(value: string) { | ||||||
| const profile = runProfileOptions.find((option) => option.id === value) | ||||||
| setDraft((current) => ({ | ||||||
| ...current, | ||||||
| runProfile: value as WorkspaceDraft['runProfile'], | ||||||
| codexSandbox: profile?.sandbox ?? current.codexSandbox, | ||||||
| codexApproval: profile?.approval ?? current.codexApproval, | ||||||
| })) | ||||||
| } | ||||||
|
|
||||||
| function handleNewWorkspace() { | ||||||
| setSelectedWorkspaceId(newWorkspaceId) | ||||||
| setDraft(emptyWorkspaceDraft(activeWorkspace ?? null)) | ||||||
|
|
@@ -376,6 +413,34 @@ export function SettingsDialog({ | |||||
| ))} | ||||||
| </select> | ||||||
| </WorkspaceField> | ||||||
| <WorkspaceField label="Run profile"> | ||||||
| <select | ||||||
| value={draft.runProfile ?? 'read-only-inspect'} | ||||||
| onChange={(event) => updateRunProfile(event.target.value)} | ||||||
| className="h-8.5 w-full rounded-lg border border-primary-200 bg-surface px-3 text-sm text-primary-900 outline-none focus-visible:ring-2 focus-visible:ring-primary-950" | ||||||
| > | ||||||
| {runProfileOptions.map((option) => ( | ||||||
| <option key={option.id} value={option.id}> | ||||||
| {option.label} | ||||||
| </option> | ||||||
| ))} | ||||||
| </select> | ||||||
| </WorkspaceField> | ||||||
| <WorkspaceField label="Approval"> | ||||||
| <select | ||||||
| value={draft.codexApproval ?? 'untrusted'} | ||||||
| onChange={(event) => | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Changing sandbox or approval individually leaves Either reset Prompt for AI agents |
||||||
| updateDraft('codexApproval', event.target.value) | ||||||
| } | ||||||
| className="h-8.5 w-full rounded-lg border border-primary-200 bg-surface px-3 text-sm text-primary-900 outline-none focus-visible:ring-2 focus-visible:ring-primary-950" | ||||||
| > | ||||||
| {approvalOptions.map((option) => ( | ||||||
| <option key={option} value={option}> | ||||||
| {option} | ||||||
| </option> | ||||||
| ))} | ||||||
| </select> | ||||||
| </WorkspaceField> | ||||||
| </div> | ||||||
|
|
||||||
| <div className="flex flex-wrap items-center justify-between gap-2"> | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P2:
updateWorkspaceresult discarded withvoid— local state updates before it, so a failure silently leaves UI and server out of sync. Add.catch()that reverts the optimistic state or surfaces an error.Prompt for AI agents