feat: integrate action mode into messaging components and enhance team provisioning banner
- Updated TeamDetailView to include action mode in message sending functionality. - Enhanced TeamProvisioningBanner to display a message based on the online status of team members. - Added ActionModeSelector to SendMessageDialog for selecting action modes during message composition. - Updated tests to validate action mode handling and ensure proper messaging behavior.
This commit is contained in:
parent
2d0d390442
commit
5e4d10b950
4 changed files with 50 additions and 8 deletions
|
|
@ -1877,12 +1877,18 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
|
||||||
sending={sendingMessage}
|
sending={sendingMessage}
|
||||||
sendError={sendMessageError}
|
sendError={sendMessageError}
|
||||||
lastResult={lastSendMessageResult}
|
lastResult={lastSendMessageResult}
|
||||||
onSend={(member, text, summary, attachments) => {
|
onSend={(member, text, summary, attachments, actionMode) => {
|
||||||
void (async () => {
|
void (async () => {
|
||||||
const sentAtMs = Date.now();
|
const sentAtMs = Date.now();
|
||||||
setPendingRepliesByMember((prev) => ({ ...prev, [member]: sentAtMs }));
|
setPendingRepliesByMember((prev) => ({ ...prev, [member]: sentAtMs }));
|
||||||
try {
|
try {
|
||||||
await sendTeamMessage(teamName, { member, text, summary, attachments });
|
await sendTeamMessage(teamName, {
|
||||||
|
member,
|
||||||
|
text,
|
||||||
|
summary,
|
||||||
|
attachments,
|
||||||
|
actionMode,
|
||||||
|
});
|
||||||
} catch {
|
} catch {
|
||||||
setPendingRepliesByMember((prev) => {
|
setPendingRepliesByMember((prev) => {
|
||||||
if (prev[member] !== sentAtMs) return prev;
|
if (prev[member] !== sentAtMs) return prev;
|
||||||
|
|
|
||||||
|
|
@ -29,10 +29,11 @@ function findProgressForTeam(
|
||||||
export const TeamProvisioningBanner = ({
|
export const TeamProvisioningBanner = ({
|
||||||
teamName,
|
teamName,
|
||||||
}: TeamProvisioningBannerProps): React.JSX.Element | null => {
|
}: TeamProvisioningBannerProps): React.JSX.Element | null => {
|
||||||
const { provisioningRuns, cancelProvisioning } = useStore(
|
const { provisioningRuns, cancelProvisioning, teamMembers } = useStore(
|
||||||
useShallow((s) => ({
|
useShallow((s) => ({
|
||||||
provisioningRuns: s.provisioningRuns,
|
provisioningRuns: s.provisioningRuns,
|
||||||
cancelProvisioning: s.cancelProvisioning,
|
cancelProvisioning: s.cancelProvisioning,
|
||||||
|
teamMembers: s.selectedTeamData?.members,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -105,14 +106,21 @@ export const TeamProvisioningBanner = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const allTeammatesOnline =
|
||||||
|
teamMembers != null &&
|
||||||
|
teamMembers.length > 0 &&
|
||||||
|
teamMembers.every((m) => m.status === 'active' || m.status === 'idle');
|
||||||
|
|
||||||
if (isReady) {
|
if (isReady) {
|
||||||
|
const readyMessage = allTeammatesOnline
|
||||||
|
? `Team launched — all ${teamMembers!.length} teammates online`
|
||||||
|
: 'Team launched — teammates may still be starting';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<div className="flex items-center gap-2 rounded-md border border-[var(--step-done-border)] bg-[var(--step-done-bg)] px-3 py-2">
|
<div className="flex items-center gap-2 rounded-md border border-[var(--step-done-border)] bg-[var(--step-done-bg)] px-3 py-2">
|
||||||
<CheckCircle2 size={14} className="shrink-0 text-[var(--step-done-text)]" />
|
<CheckCircle2 size={14} className="shrink-0 text-[var(--step-done-text)]" />
|
||||||
<p className="flex-1 text-xs text-[var(--step-success-text)]">
|
<p className="flex-1 text-xs text-[var(--step-success-text)]">{readyMessage}</p>
|
||||||
Team launched — teammates may still be starting
|
|
||||||
</p>
|
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||||
import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer';
|
import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer';
|
||||||
import { AttachmentPreviewList } from '@renderer/components/team/attachments/AttachmentPreviewList';
|
import { AttachmentPreviewList } from '@renderer/components/team/attachments/AttachmentPreviewList';
|
||||||
import { DropZoneOverlay } from '@renderer/components/team/attachments/DropZoneOverlay';
|
import { DropZoneOverlay } from '@renderer/components/team/attachments/DropZoneOverlay';
|
||||||
|
import { ActionModeSelector } from '@renderer/components/team/messages/ActionModeSelector';
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
|
|
@ -28,6 +29,7 @@ import { AlertCircle, ImagePlus, Send, X } from 'lucide-react';
|
||||||
|
|
||||||
import { MemberBadge } from '../MemberBadge';
|
import { MemberBadge } from '../MemberBadge';
|
||||||
|
|
||||||
|
import type { ActionMode } from '@renderer/components/team/messages/ActionModeSelector';
|
||||||
import type { InlineChip } from '@renderer/types/inlineChip';
|
import type { InlineChip } from '@renderer/types/inlineChip';
|
||||||
import type { MentionSuggestion } from '@renderer/types/mention';
|
import type { MentionSuggestion } from '@renderer/types/mention';
|
||||||
import type { AttachmentPayload, ResolvedTeamMember, SendMessageResult } from '@shared/types';
|
import type { AttachmentPayload, ResolvedTeamMember, SendMessageResult } from '@shared/types';
|
||||||
|
|
@ -55,7 +57,8 @@ interface SendMessageDialogProps {
|
||||||
member: string,
|
member: string,
|
||||||
text: string,
|
text: string,
|
||||||
summary?: string,
|
summary?: string,
|
||||||
attachments?: AttachmentPayload[]
|
attachments?: AttachmentPayload[],
|
||||||
|
actionMode?: ActionMode
|
||||||
) => void;
|
) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
@ -90,6 +93,7 @@ export const SendMessageDialog = ({
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const [imageRestrictionError, setImageRestrictionError] = useState<string | null>(null);
|
const [imageRestrictionError, setImageRestrictionError] = useState<string | null>(null);
|
||||||
const imageRestrictionTimerRef = useRef(0);
|
const imageRestrictionTimerRef = useRef(0);
|
||||||
|
const [actionMode, setActionMode] = useState<ActionMode>('do');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
attachments,
|
attachments,
|
||||||
|
|
@ -107,6 +111,14 @@ export const SendMessageDialog = ({
|
||||||
const supportsAttachments = isLeadRecipient && !!isTeamAlive;
|
const supportsAttachments = isLeadRecipient && !!isTeamAlive;
|
||||||
const canAttach = supportsAttachments && canAddMore;
|
const canAttach = supportsAttachments && canAddMore;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isLeadRecipient) {
|
||||||
|
setActionMode('delegate');
|
||||||
|
} else {
|
||||||
|
setActionMode((prev) => (prev === 'delegate' ? 'do' : prev));
|
||||||
|
}
|
||||||
|
}, [isLeadRecipient]);
|
||||||
|
|
||||||
const [pendingAutoClose, setPendingAutoClose] = useState(false);
|
const [pendingAutoClose, setPendingAutoClose] = useState(false);
|
||||||
// Reset form on open transition (avoid setState in render)
|
// Reset form on open transition (avoid setState in render)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -195,7 +207,13 @@ export const SendMessageDialog = ({
|
||||||
|
|
||||||
const handleSubmit = (): void => {
|
const handleSubmit = (): void => {
|
||||||
if (!canSend) return;
|
if (!canSend) return;
|
||||||
onSend(member.trim(), finalText, trimmedText, attachments.length > 0 ? attachments : undefined);
|
onSend(
|
||||||
|
member.trim(),
|
||||||
|
finalText,
|
||||||
|
trimmedText,
|
||||||
|
attachments.length > 0 ? attachments : undefined,
|
||||||
|
actionMode
|
||||||
|
);
|
||||||
textDraft.clearDraft();
|
textDraft.clearDraft();
|
||||||
chipDraft.clearChipDraft();
|
chipDraft.clearChipDraft();
|
||||||
clearAttachments();
|
clearAttachments();
|
||||||
|
|
@ -428,6 +446,13 @@ export const SendMessageDialog = ({
|
||||||
maxRows={12}
|
maxRows={12}
|
||||||
maxLength={MAX_TEXT_LENGTH}
|
maxLength={MAX_TEXT_LENGTH}
|
||||||
disabled={sending}
|
disabled={sending}
|
||||||
|
cornerActionLeft={
|
||||||
|
<ActionModeSelector
|
||||||
|
value={actionMode}
|
||||||
|
onChange={setActionMode}
|
||||||
|
showDelegate={isLeadRecipient}
|
||||||
|
/>
|
||||||
|
}
|
||||||
cornerAction={
|
cornerAction={
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
||||||
|
|
@ -276,6 +276,9 @@ describe('TeamProvisioningService post-compact lifecycle', () => {
|
||||||
// Should contain persistent context
|
// Should contain persistent context
|
||||||
expect(text).toContain('Constraints:');
|
expect(text).toContain('Constraints:');
|
||||||
expect(text).toContain('Do NOT call TeamDelete');
|
expect(text).toContain('Do NOT call TeamDelete');
|
||||||
|
expect(text).toContain('TURN ACTION MODE PROTOCOL (HIGHEST PRIORITY FOR EACH USER TURN):');
|
||||||
|
expect(text).toContain('ASK: Strict read-only conversation mode.');
|
||||||
|
expect(text).toContain('DELEGATE: Strict orchestration mode for leads.');
|
||||||
expect(text).toContain('cross_team_send');
|
expect(text).toContain('cross_team_send');
|
||||||
expect(text).toContain('cross_team_list_targets');
|
expect(text).toContain('cross_team_list_targets');
|
||||||
expect(text).toContain('cross_team_get_outbox');
|
expect(text).toContain('cross_team_get_outbox');
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue