feat: add effort level management to team provisioning and dialogs
- Introduced EffortLevel type to define valid effort levels ('low', 'medium', 'high').
- Updated TeamCreateRequest and TeamLaunchRequest interfaces to include effort parameter.
- Enhanced CreateTeamDialog and LaunchTeamDialog to allow users to select effort levels.
- Integrated effort level handling in provisioning requests and responses within TeamProvisioningService.
- Removed auto-focus effect in ToolApprovalSheet for improved user experience.
This commit is contained in:
parent
3e605622f7
commit
964a8772ea
10 changed files with 215 additions and 11 deletions
|
|
@ -96,6 +96,7 @@ import type {
|
||||||
AttachmentMeta,
|
AttachmentMeta,
|
||||||
AttachmentPayload,
|
AttachmentPayload,
|
||||||
CreateTaskRequest,
|
CreateTaskRequest,
|
||||||
|
EffortLevel,
|
||||||
GlobalTask,
|
GlobalTask,
|
||||||
IpcResult,
|
IpcResult,
|
||||||
KanbanColumnId,
|
KanbanColumnId,
|
||||||
|
|
@ -548,6 +549,12 @@ function isProvisioningTeamName(teamName: string): boolean {
|
||||||
return parts.every((p) => /^[a-z0-9]+$/.test(p));
|
return parts.every((p) => /^[a-z0-9]+$/.test(p));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const VALID_EFFORT_LEVELS: readonly string[] = ['low', 'medium', 'high'];
|
||||||
|
|
||||||
|
function isValidEffort(value: unknown): value is EffortLevel {
|
||||||
|
return typeof value === 'string' && VALID_EFFORT_LEVELS.includes(value);
|
||||||
|
}
|
||||||
|
|
||||||
async function validateProvisioningRequest(
|
async function validateProvisioningRequest(
|
||||||
request: unknown
|
request: unknown
|
||||||
): Promise<{ valid: true; value: TeamCreateRequest } | { valid: false; error: string }> {
|
): Promise<{ valid: true; value: TeamCreateRequest } | { valid: false; error: string }> {
|
||||||
|
|
@ -645,6 +652,7 @@ async function validateProvisioningRequest(
|
||||||
cwd,
|
cwd,
|
||||||
prompt: typeof payload.prompt === 'string' ? payload.prompt.trim() || undefined : undefined,
|
prompt: typeof payload.prompt === 'string' ? payload.prompt.trim() || undefined : undefined,
|
||||||
model: typeof payload.model === 'string' ? payload.model.trim() || undefined : undefined,
|
model: typeof payload.model === 'string' ? payload.model.trim() || undefined : undefined,
|
||||||
|
effort: isValidEffort(payload.effort) ? payload.effort : undefined,
|
||||||
skipPermissions:
|
skipPermissions:
|
||||||
typeof payload.skipPermissions === 'boolean' ? payload.skipPermissions : undefined,
|
typeof payload.skipPermissions === 'boolean' ? payload.skipPermissions : undefined,
|
||||||
},
|
},
|
||||||
|
|
@ -751,6 +759,7 @@ async function handleLaunchTeam(
|
||||||
cwd,
|
cwd,
|
||||||
prompt: typeof payload.prompt === 'string' ? payload.prompt.trim() || undefined : undefined,
|
prompt: typeof payload.prompt === 'string' ? payload.prompt.trim() || undefined : undefined,
|
||||||
model: typeof payload.model === 'string' ? payload.model.trim() || undefined : undefined,
|
model: typeof payload.model === 'string' ? payload.model.trim() || undefined : undefined,
|
||||||
|
effort: isValidEffort(payload.effort) ? payload.effort : undefined,
|
||||||
clearContext: payload.clearContext === true ? true : undefined,
|
clearContext: payload.clearContext === true ? true : undefined,
|
||||||
skipPermissions:
|
skipPermissions:
|
||||||
typeof payload.skipPermissions === 'boolean' ? payload.skipPermissions : undefined,
|
typeof payload.skipPermissions === 'boolean' ? payload.skipPermissions : undefined,
|
||||||
|
|
|
||||||
|
|
@ -1804,6 +1804,7 @@ export class TeamProvisioningService {
|
||||||
'TeamDelete,TodoWrite',
|
'TeamDelete,TodoWrite',
|
||||||
...(request.skipPermissions !== false ? ['--dangerously-skip-permissions'] : []),
|
...(request.skipPermissions !== false ? ['--dangerously-skip-permissions'] : []),
|
||||||
...(request.model ? ['--model', request.model] : []),
|
...(request.model ? ['--model', request.model] : []),
|
||||||
|
...(request.effort ? ['--effort', request.effort] : []),
|
||||||
];
|
];
|
||||||
try {
|
try {
|
||||||
child = spawnCli(claudePath, spawnArgs, {
|
child = spawnCli(claudePath, spawnArgs, {
|
||||||
|
|
@ -2137,6 +2138,9 @@ export class TeamProvisioningService {
|
||||||
if (request.model) {
|
if (request.model) {
|
||||||
launchArgs.push('--model', request.model);
|
launchArgs.push('--model', request.model);
|
||||||
}
|
}
|
||||||
|
if (request.effort) {
|
||||||
|
launchArgs.push('--effort', request.effort);
|
||||||
|
}
|
||||||
// New sessions: CLI creates its own ID. No --resume with synthetic name — docs say
|
// New sessions: CLI creates its own ID. No --resume with synthetic name — docs say
|
||||||
// --resume is for existing sessions and may show an interactive picker if not found.
|
// --resume is for existing sessions and may show an interactive picker if not found.
|
||||||
|
|
||||||
|
|
@ -3222,7 +3226,8 @@ export class TeamProvisioningService {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles a control_request message from CLI stream-json output.
|
* Handles a control_request message from CLI stream-json output.
|
||||||
* Only `can_use_tool` subtype is processed — others are logged and ignored.
|
* `can_use_tool` → emits to renderer for manual approval.
|
||||||
|
* All other subtypes (hook_callback, etc.) → auto-allowed to prevent deadlock.
|
||||||
*/
|
*/
|
||||||
private handleControlRequest(run: ProvisioningRun, msg: Record<string, unknown>): void {
|
private handleControlRequest(run: ProvisioningRun, msg: Record<string, unknown>): void {
|
||||||
const requestId = typeof msg.request_id === 'string' ? msg.request_id : null;
|
const requestId = typeof msg.request_id === 'string' ? msg.request_id : null;
|
||||||
|
|
@ -3233,10 +3238,14 @@ export class TeamProvisioningService {
|
||||||
|
|
||||||
const request = msg.request as Record<string, unknown> | undefined;
|
const request = msg.request as Record<string, unknown> | undefined;
|
||||||
const subtype = request?.subtype;
|
const subtype = request?.subtype;
|
||||||
|
|
||||||
|
// Non-`can_use_tool` subtypes (hook_callback, etc.) are auto-allowed to prevent
|
||||||
|
// CLI deadlock — hooks are user-configured and should not block on manual approval.
|
||||||
if (subtype !== 'can_use_tool') {
|
if (subtype !== 'can_use_tool') {
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`[${run.teamName}] control_request subtype=${String(subtype)}, ignoring (only can_use_tool supported)`
|
`[${run.teamName}] control_request subtype=${String(subtype)}, auto-allowing to prevent deadlock`
|
||||||
);
|
);
|
||||||
|
this.autoAllowControlRequest(run, requestId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -3257,6 +3266,34 @@ export class TeamProvisioningService {
|
||||||
this.emitToolApprovalEvent(approval);
|
this.emitToolApprovalEvent(approval);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Immediately sends an "allow" control_response for a non-tool control_request.
|
||||||
|
* Prevents CLI deadlock for hook_callback and other non-`can_use_tool` subtypes.
|
||||||
|
*/
|
||||||
|
private autoAllowControlRequest(run: ProvisioningRun, requestId: string): void {
|
||||||
|
if (!run.child?.stdin?.writable) {
|
||||||
|
logger.warn(`[${run.teamName}] Cannot auto-allow control_request: stdin not writable`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
type: 'control_response',
|
||||||
|
response: {
|
||||||
|
subtype: 'success',
|
||||||
|
request_id: requestId,
|
||||||
|
response: { behavior: 'allow' },
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
run.child.stdin.write(JSON.stringify(response) + '\n', (err) => {
|
||||||
|
if (err) {
|
||||||
|
logger.error(
|
||||||
|
`[${run.teamName}] Failed to auto-allow control_request ${requestId}: ${err.message}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Respond to a pending tool approval — sends control_response to CLI stdin.
|
* Respond to a pending tool approval — sends control_response to CLI stdin.
|
||||||
* Validates runId match and requestId existence before writing.
|
* Validates runId match and requestId existence before writing.
|
||||||
|
|
|
||||||
|
|
@ -82,13 +82,6 @@ export const ToolApprovalSheet: React.FC = () => {
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
const [disabled, setDisabled] = useState(false);
|
const [disabled, setDisabled] = useState(false);
|
||||||
|
|
||||||
// Auto-focus when new approval arrives
|
|
||||||
useEffect(() => {
|
|
||||||
if (current && containerRef.current) {
|
|
||||||
containerRef.current.focus();
|
|
||||||
}
|
|
||||||
}, [current?.requestId]); // eslint-disable-line react-hooks/exhaustive-deps
|
|
||||||
|
|
||||||
const handleRespond = useCallback(
|
const handleRespond = useCallback(
|
||||||
(allow: boolean) => {
|
(allow: boolean) => {
|
||||||
if (!current || disabled) return;
|
if (!current || disabled) return;
|
||||||
|
|
|
||||||
|
|
@ -54,6 +54,8 @@ interface ActivityItemProps {
|
||||||
zebraShade?: boolean;
|
zebraShade?: boolean;
|
||||||
/** When true, collapse message body — show only header with expand chevron. */
|
/** When true, collapse message body — show only header with expand chevron. */
|
||||||
forceCollapsed?: boolean;
|
forceCollapsed?: boolean;
|
||||||
|
/** Called when user toggles expand/collapse in collapsed mode. Presence enables chevron. */
|
||||||
|
onCollapseToggle?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStringField(obj: StructuredMessage, key: string): string | null {
|
function getStringField(obj: StructuredMessage, key: string): string | null {
|
||||||
|
|
@ -216,6 +218,7 @@ export const ActivityItem = ({
|
||||||
onRestartTeam,
|
onRestartTeam,
|
||||||
zebraShade,
|
zebraShade,
|
||||||
forceCollapsed,
|
forceCollapsed,
|
||||||
|
onCollapseToggle,
|
||||||
}: ActivityItemProps): React.JSX.Element => {
|
}: ActivityItemProps): React.JSX.Element => {
|
||||||
const colors = getTeamColorSet(memberColor ?? message.color ?? '');
|
const colors = getTeamColorSet(memberColor ?? message.color ?? '');
|
||||||
const formattedRole = formatAgentRole(memberRole);
|
const formattedRole = formatAgentRole(memberRole);
|
||||||
|
|
@ -295,8 +298,9 @@ export const ActivityItem = ({
|
||||||
onCreateTask?.(subject, description);
|
onCreateTask?.(subject, description);
|
||||||
};
|
};
|
||||||
|
|
||||||
const isHeaderClickable = Boolean(systemLabel) || forceCollapsed === true;
|
const isHeaderClickable =
|
||||||
const showChevron = Boolean(systemLabel) || forceCollapsed === true;
|
Boolean(systemLabel) || forceCollapsed === true || onCollapseToggle != null;
|
||||||
|
const showChevron = isHeaderClickable;
|
||||||
const isUserSent = message.source === 'user_sent';
|
const isUserSent = message.source === 'user_sent';
|
||||||
const isSystemMessage = message.from === 'system';
|
const isSystemMessage = message.from === 'system';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ import { normalizePath } from '@renderer/utils/pathNormalize';
|
||||||
import { getMemberColor } from '@shared/constants/memberColors';
|
import { getMemberColor } from '@shared/constants/memberColors';
|
||||||
import { AlertTriangle, CheckCircle2, Info, Loader2, X } from 'lucide-react';
|
import { AlertTriangle, CheckCircle2, Info, Loader2, X } from 'lucide-react';
|
||||||
|
|
||||||
|
import { EffortLevelSelector } from './EffortLevelSelector';
|
||||||
import { ExtendedContextCheckbox } from './ExtendedContextCheckbox';
|
import { ExtendedContextCheckbox } from './ExtendedContextCheckbox';
|
||||||
import { ProjectPathSelector } from './ProjectPathSelector';
|
import { ProjectPathSelector } from './ProjectPathSelector';
|
||||||
import { SkipPermissionsCheckbox } from './SkipPermissionsCheckbox';
|
import { SkipPermissionsCheckbox } from './SkipPermissionsCheckbox';
|
||||||
|
|
@ -50,6 +51,7 @@ const TEAM_COLOR_NAMES = [
|
||||||
|
|
||||||
import type { MentionSuggestion } from '@renderer/types/mention';
|
import type { MentionSuggestion } from '@renderer/types/mention';
|
||||||
import type {
|
import type {
|
||||||
|
EffortLevel,
|
||||||
Project,
|
Project,
|
||||||
TeamCreateRequest,
|
TeamCreateRequest,
|
||||||
TeamProvisioningMemberInput,
|
TeamProvisioningMemberInput,
|
||||||
|
|
@ -237,6 +239,9 @@ export const CreateTeamDialog = ({
|
||||||
const [skipPermissions, setSkipPermissionsRaw] = useState(
|
const [skipPermissions, setSkipPermissionsRaw] = useState(
|
||||||
() => localStorage.getItem('team:lastSkipPermissions') !== 'false'
|
() => localStorage.getItem('team:lastSkipPermissions') !== 'false'
|
||||||
);
|
);
|
||||||
|
const [selectedEffort, setSelectedEffortRaw] = useState(
|
||||||
|
() => localStorage.getItem('team:lastSelectedEffort') ?? ''
|
||||||
|
);
|
||||||
|
|
||||||
const setSelectedModel = (value: string): void => {
|
const setSelectedModel = (value: string): void => {
|
||||||
setSelectedModelRaw(value);
|
setSelectedModelRaw(value);
|
||||||
|
|
@ -253,6 +258,11 @@ export const CreateTeamDialog = ({
|
||||||
localStorage.setItem('team:lastSkipPermissions', String(value));
|
localStorage.setItem('team:lastSkipPermissions', String(value));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setSelectedEffort = (value: string): void => {
|
||||||
|
setSelectedEffortRaw(value);
|
||||||
|
localStorage.setItem('team:lastSelectedEffort', value);
|
||||||
|
};
|
||||||
|
|
||||||
const resetUIState = (): void => {
|
const resetUIState = (): void => {
|
||||||
setLocalError(null);
|
setLocalError(null);
|
||||||
setFieldErrors({});
|
setFieldErrors({});
|
||||||
|
|
@ -482,6 +492,7 @@ export const CreateTeamDialog = ({
|
||||||
cwd: effectiveCwd,
|
cwd: effectiveCwd,
|
||||||
prompt: prompt.trim() || undefined,
|
prompt: prompt.trim() || undefined,
|
||||||
model: effectiveModel,
|
model: effectiveModel,
|
||||||
|
effort: (selectedEffort as EffortLevel) || undefined,
|
||||||
skipPermissions,
|
skipPermissions,
|
||||||
}),
|
}),
|
||||||
[
|
[
|
||||||
|
|
@ -493,6 +504,7 @@ export const CreateTeamDialog = ({
|
||||||
effectiveCwd,
|
effectiveCwd,
|
||||||
prompt,
|
prompt,
|
||||||
effectiveModel,
|
effectiveModel,
|
||||||
|
selectedEffort,
|
||||||
skipPermissions,
|
skipPermissions,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
@ -806,6 +818,11 @@ export const CreateTeamDialog = ({
|
||||||
onValueChange={setSelectedModel}
|
onValueChange={setSelectedModel}
|
||||||
id="create-model"
|
id="create-model"
|
||||||
/>
|
/>
|
||||||
|
<EffortLevelSelector
|
||||||
|
value={selectedEffort}
|
||||||
|
onValueChange={setSelectedEffort}
|
||||||
|
id="create-effort"
|
||||||
|
/>
|
||||||
<ExtendedContextCheckbox
|
<ExtendedContextCheckbox
|
||||||
id="create-extended-context"
|
id="create-extended-context"
|
||||||
checked={extendedContext}
|
checked={extendedContext}
|
||||||
|
|
|
||||||
51
src/renderer/components/team/dialogs/EffortLevelSelector.tsx
Normal file
51
src/renderer/components/team/dialogs/EffortLevelSelector.tsx
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Label } from '@renderer/components/ui/label';
|
||||||
|
import { cn } from '@renderer/lib/utils';
|
||||||
|
|
||||||
|
const EFFORT_OPTIONS = [
|
||||||
|
{ value: '', label: 'Default' },
|
||||||
|
{ value: 'low', label: 'Low' },
|
||||||
|
{ value: 'medium', label: 'Medium' },
|
||||||
|
{ value: 'high', label: 'High' },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
export interface EffortLevelSelectorProps {
|
||||||
|
value: string;
|
||||||
|
onValueChange: (value: string) => void;
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const EffortLevelSelector: React.FC<EffortLevelSelectorProps> = ({
|
||||||
|
value,
|
||||||
|
onValueChange,
|
||||||
|
id,
|
||||||
|
}) => (
|
||||||
|
<div className="mb-3">
|
||||||
|
<Label htmlFor={id} className="label-optional mb-1.5 block">
|
||||||
|
Effort level (optional)
|
||||||
|
</Label>
|
||||||
|
<div className="inline-flex rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] p-0.5">
|
||||||
|
{EFFORT_OPTIONS.map((opt) => (
|
||||||
|
<button
|
||||||
|
key={opt.value || '__default__'}
|
||||||
|
type="button"
|
||||||
|
id={opt.value === value ? id : undefined}
|
||||||
|
className={cn(
|
||||||
|
'rounded-[3px] px-3 py-1 text-xs font-medium transition-colors',
|
||||||
|
value === opt.value
|
||||||
|
? 'bg-[var(--color-surface-raised)] text-[var(--color-text)] shadow-sm'
|
||||||
|
: 'text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]'
|
||||||
|
)}
|
||||||
|
onClick={() => onValueChange(opt.value)}
|
||||||
|
>
|
||||||
|
{opt.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<p className="mt-1 text-[11px] text-[var(--color-text-muted)]">
|
||||||
|
Controls how much reasoning Claude invests before responding. Default uses Claude's
|
||||||
|
standard behavior.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
@ -24,12 +24,14 @@ import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
|
||||||
import { normalizePath } from '@renderer/utils/pathNormalize';
|
import { normalizePath } from '@renderer/utils/pathNormalize';
|
||||||
import { AlertTriangle, CheckCircle2, Loader2, RotateCcw, X } from 'lucide-react';
|
import { AlertTriangle, CheckCircle2, Loader2, RotateCcw, X } from 'lucide-react';
|
||||||
|
|
||||||
|
import { EffortLevelSelector } from './EffortLevelSelector';
|
||||||
import { ProjectPathSelector } from './ProjectPathSelector';
|
import { ProjectPathSelector } from './ProjectPathSelector';
|
||||||
import { computeEffectiveTeamModel, TeamModelSelector } from './TeamModelSelector';
|
import { computeEffectiveTeamModel, TeamModelSelector } from './TeamModelSelector';
|
||||||
|
|
||||||
import type { ActiveTeamRef } from './CreateTeamDialog';
|
import type { ActiveTeamRef } from './CreateTeamDialog';
|
||||||
import type { MentionSuggestion } from '@renderer/types/mention';
|
import type { MentionSuggestion } from '@renderer/types/mention';
|
||||||
import type {
|
import type {
|
||||||
|
EffortLevel,
|
||||||
Project,
|
Project,
|
||||||
ResolvedTeamMember,
|
ResolvedTeamMember,
|
||||||
TeamLaunchRequest,
|
TeamLaunchRequest,
|
||||||
|
|
@ -82,6 +84,9 @@ export const LaunchTeamDialog = ({
|
||||||
const [skipPermissions, setSkipPermissionsRaw] = useState(
|
const [skipPermissions, setSkipPermissionsRaw] = useState(
|
||||||
() => localStorage.getItem('team:lastSkipPermissions') !== 'false'
|
() => localStorage.getItem('team:lastSkipPermissions') !== 'false'
|
||||||
);
|
);
|
||||||
|
const [selectedEffort, setSelectedEffortRaw] = useState(
|
||||||
|
() => localStorage.getItem('team:lastSelectedEffort') ?? ''
|
||||||
|
);
|
||||||
const [clearContext, setClearContext] = useState(false);
|
const [clearContext, setClearContext] = useState(false);
|
||||||
const [conflictDismissed, setConflictDismissed] = useState(false);
|
const [conflictDismissed, setConflictDismissed] = useState(false);
|
||||||
|
|
||||||
|
|
@ -100,6 +105,11 @@ export const LaunchTeamDialog = ({
|
||||||
localStorage.setItem('team:lastSkipPermissions', String(value));
|
localStorage.setItem('team:lastSkipPermissions', String(value));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setSelectedEffort = (value: string): void => {
|
||||||
|
setSelectedEffortRaw(value);
|
||||||
|
localStorage.setItem('team:lastSelectedEffort', value);
|
||||||
|
};
|
||||||
|
|
||||||
const resetFormState = (): void => {
|
const resetFormState = (): void => {
|
||||||
setLocalError(null);
|
setLocalError(null);
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
|
|
@ -291,6 +301,7 @@ export const LaunchTeamDialog = ({
|
||||||
cwd: effectiveCwd,
|
cwd: effectiveCwd,
|
||||||
prompt: promptDraft.value.trim() || undefined,
|
prompt: promptDraft.value.trim() || undefined,
|
||||||
model: computeEffectiveTeamModel(selectedModel, extendedContext),
|
model: computeEffectiveTeamModel(selectedModel, extendedContext),
|
||||||
|
effort: (selectedEffort as EffortLevel) || undefined,
|
||||||
clearContext: clearContext || undefined,
|
clearContext: clearContext || undefined,
|
||||||
skipPermissions,
|
skipPermissions,
|
||||||
});
|
});
|
||||||
|
|
@ -435,6 +446,11 @@ export const LaunchTeamDialog = ({
|
||||||
onValueChange={setSelectedModel}
|
onValueChange={setSelectedModel}
|
||||||
id="launch-model"
|
id="launch-model"
|
||||||
/>
|
/>
|
||||||
|
<EffortLevelSelector
|
||||||
|
value={selectedEffort}
|
||||||
|
onValueChange={setSelectedEffort}
|
||||||
|
id="launch-effort"
|
||||||
|
/>
|
||||||
<ExtendedContextCheckbox
|
<ExtendedContextCheckbox
|
||||||
id="launch-extended-context"
|
id="launch-extended-context"
|
||||||
checked={extendedContext}
|
checked={extendedContext}
|
||||||
|
|
|
||||||
34
src/renderer/hooks/useTeamMessagesExpanded.ts
Normal file
34
src/renderer/hooks/useTeamMessagesExpanded.ts
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { useCallback, useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
addExpanded,
|
||||||
|
getExpandedOverrides,
|
||||||
|
removeExpanded,
|
||||||
|
} from '@renderer/utils/teamMessageExpandStorage';
|
||||||
|
|
||||||
|
export function useTeamMessagesExpanded(teamName: string): {
|
||||||
|
expandedSet: Set<string>;
|
||||||
|
toggle: (messageKey: string) => void;
|
||||||
|
} {
|
||||||
|
const [version, setVersion] = useState(0);
|
||||||
|
const expandedSet = useMemo(() => {
|
||||||
|
if (version < 0) return new Set<string>();
|
||||||
|
return teamName ? getExpandedOverrides(teamName) : new Set<string>();
|
||||||
|
}, [teamName, version]);
|
||||||
|
|
||||||
|
const toggle = useCallback(
|
||||||
|
(messageKey: string) => {
|
||||||
|
if (!teamName) return;
|
||||||
|
const existing = getExpandedOverrides(teamName);
|
||||||
|
if (existing.has(messageKey)) {
|
||||||
|
removeExpanded(teamName, messageKey);
|
||||||
|
} else {
|
||||||
|
addExpanded(teamName, messageKey);
|
||||||
|
}
|
||||||
|
setVersion((v) => v + 1);
|
||||||
|
},
|
||||||
|
[teamName]
|
||||||
|
);
|
||||||
|
|
||||||
|
return { expandedSet, toggle };
|
||||||
|
}
|
||||||
39
src/renderer/utils/teamMessageExpandStorage.ts
Normal file
39
src/renderer/utils/teamMessageExpandStorage.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
const STORAGE_PREFIX = 'team-msg-expanded:';
|
||||||
|
|
||||||
|
function storageKey(teamName: string): string {
|
||||||
|
return `${STORAGE_PREFIX}${teamName}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getExpandedOverrides(teamName: string): Set<string> {
|
||||||
|
try {
|
||||||
|
const raw = localStorage.getItem(storageKey(teamName));
|
||||||
|
if (!raw) return new Set();
|
||||||
|
const arr = JSON.parse(raw) as unknown;
|
||||||
|
if (!Array.isArray(arr)) return new Set();
|
||||||
|
return new Set(arr.filter((x): x is string => typeof x === 'string'));
|
||||||
|
} catch {
|
||||||
|
return new Set();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function addExpanded(teamName: string, messageKey: string): void {
|
||||||
|
try {
|
||||||
|
const set = getExpandedOverrides(teamName);
|
||||||
|
if (set.has(messageKey)) return;
|
||||||
|
set.add(messageKey);
|
||||||
|
localStorage.setItem(storageKey(teamName), JSON.stringify([...set]));
|
||||||
|
} catch {
|
||||||
|
// quota or disabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function removeExpanded(teamName: string, messageKey: string): void {
|
||||||
|
try {
|
||||||
|
const set = getExpandedOverrides(teamName);
|
||||||
|
if (!set.has(messageKey)) return;
|
||||||
|
set.delete(messageKey);
|
||||||
|
localStorage.setItem(storageKey(teamName), JSON.stringify([...set]));
|
||||||
|
} catch {
|
||||||
|
// quota or disabled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -296,11 +296,14 @@ export interface TeamData {
|
||||||
isAlive?: boolean;
|
isAlive?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type EffortLevel = 'low' | 'medium' | 'high';
|
||||||
|
|
||||||
export interface TeamLaunchRequest {
|
export interface TeamLaunchRequest {
|
||||||
teamName: string;
|
teamName: string;
|
||||||
cwd: string;
|
cwd: string;
|
||||||
prompt?: string;
|
prompt?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
effort?: EffortLevel;
|
||||||
/** When true, skip --resume and start a fresh session (clears context memory). */
|
/** When true, skip --resume and start a fresh session (clears context memory). */
|
||||||
clearContext?: boolean;
|
clearContext?: boolean;
|
||||||
/** When false, run WITHOUT --dangerously-skip-permissions (manual tool approval). Default: true. */
|
/** When false, run WITHOUT --dangerously-skip-permissions (manual tool approval). Default: true. */
|
||||||
|
|
@ -385,6 +388,7 @@ export interface TeamCreateRequest {
|
||||||
cwd: string;
|
cwd: string;
|
||||||
prompt?: string;
|
prompt?: string;
|
||||||
model?: string;
|
model?: string;
|
||||||
|
effort?: EffortLevel;
|
||||||
/** When false, run WITHOUT --dangerously-skip-permissions (manual tool approval). Default: true. */
|
/** When false, run WITHOUT --dangerously-skip-permissions (manual tool approval). Default: true. */
|
||||||
skipPermissions?: boolean;
|
skipPermissions?: boolean;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue