diff --git a/backend/internal/service/session/service.go b/backend/internal/service/session/service.go index 32d338897a..c794db99e4 100644 --- a/backend/internal/service/session/service.go +++ b/backend/internal/service/session/service.go @@ -6,6 +6,7 @@ import ( "fmt" "strings" "time" + "unicode/utf8" "github.com/aoagents/agent-orchestrator/backend/internal/domain" "github.com/aoagents/agent-orchestrator/backend/internal/httpd/apierr" @@ -77,6 +78,8 @@ type scmProvider interface { FetchReviewThreads(ctx context.Context, ref ports.SCMPRRef) (ports.SCMReviewObservation, error) } +const maxDisplayNameLen = 20 + // Service is the controller-facing session service. It delegates command-side // session operations to the internal sessionmanager.Manager and owns read-model // assembly, including user-facing display status derivation. @@ -325,6 +328,9 @@ func (s *Service) Rename(ctx context.Context, id domain.SessionID, displayName s if displayName == "" { return apierr.Invalid("DISPLAY_NAME_REQUIRED", "Display name is required", nil) } + if utf8.RuneCountInString(displayName) > maxDisplayNameLen { + return apierr.Invalid("DISPLAY_NAME_TOO_LONG", "Display name must be 20 characters or fewer", nil) + } renamed, err := s.store.RenameSession(ctx, id, displayName, time.Now().UTC()) if err != nil { return fmt.Errorf("rename %s: %w", id, err) diff --git a/backend/internal/service/session/service_test.go b/backend/internal/service/session/service_test.go index c5b1b1ca64..263705c63b 100644 --- a/backend/internal/service/session/service_test.go +++ b/backend/internal/service/session/service_test.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "strings" "testing" "time" @@ -164,6 +165,20 @@ func TestSessionRenameUpdatesDisplayName(t *testing.T) { } } +func TestSessionRenameRejectsTooLongDisplayName(t *testing.T) { + st := newFakeStore() + st.sessions["mer-1"] = domain.SessionRecord{ID: "mer-1", ProjectID: "mer", DisplayName: "Original"} + + err := (&Service{store: st}).Rename(context.Background(), "mer-1", strings.Repeat("x", 21)) + var e *apierr.Error + if !errors.As(err, &e) || e.Kind != apierr.KindInvalid || e.Code != "DISPLAY_NAME_TOO_LONG" { + t.Fatalf("err = %v, want apierr Invalid DISPLAY_NAME_TOO_LONG", err) + } + if got := st.sessions["mer-1"].DisplayName; got != "Original" { + t.Fatalf("display name = %q, want unchanged after rejected rename", got) + } +} + func TestSessionSetPreviewPersistsURL(t *testing.T) { st := newFakeStore() st.sessions["mer-1"] = domain.SessionRecord{ID: "mer-1", ProjectID: "mer"} diff --git a/frontend/src/renderer/components/CreateProjectAgentSheet.tsx b/frontend/src/renderer/components/CreateProjectAgentSheet.tsx index 76f6054ede..a2c52fe730 100644 --- a/frontend/src/renderer/components/CreateProjectAgentSheet.tsx +++ b/frontend/src/renderer/components/CreateProjectAgentSheet.tsx @@ -142,4 +142,4 @@ export const RequiredAgentField = memo(function RequiredAgentField({ ); -}); \ No newline at end of file +}); diff --git a/frontend/src/renderer/components/ProjectSettingsForm.test.tsx b/frontend/src/renderer/components/ProjectSettingsForm.test.tsx index 26776054dc..27ecb900ee 100644 --- a/frontend/src/renderer/components/ProjectSettingsForm.test.tsx +++ b/frontend/src/renderer/components/ProjectSettingsForm.test.tsx @@ -210,4 +210,4 @@ describe("ProjectSettingsForm", () => { }); expect(await screen.findByText("Saved.")).toBeInTheDocument(); }); -}); \ No newline at end of file +}); diff --git a/frontend/src/renderer/components/ProjectSettingsForm.tsx b/frontend/src/renderer/components/ProjectSettingsForm.tsx index ea51b7fd4d..5c504e0d60 100644 --- a/frontend/src/renderer/components/ProjectSettingsForm.tsx +++ b/frontend/src/renderer/components/ProjectSettingsForm.tsx @@ -300,4 +300,4 @@ function CenteredNote({ children }: { children: React.ReactNode }) { // rather than an empty {} the daemon would persist. function blankToUndefined(obj: T): T | undefined { return Object.values(obj).some((v) => v !== undefined) ? obj : undefined; -} \ No newline at end of file +} diff --git a/frontend/src/renderer/lib/agent-options.ts b/frontend/src/renderer/lib/agent-options.ts index 7cdc56eeee..39ade7eaec 100644 --- a/frontend/src/renderer/lib/agent-options.ts +++ b/frontend/src/renderer/lib/agent-options.ts @@ -26,4 +26,4 @@ export const AGENT_OPTIONS = [ // The agent new projects use by default, and the fallback for worker/orchestrator // role fields that have no explicit configuration. Users can change it per project. -export const DEFAULT_PROJECT_AGENT: (typeof AGENT_OPTIONS)[number] = "claude-code"; \ No newline at end of file +export const DEFAULT_PROJECT_AGENT: (typeof AGENT_OPTIONS)[number] = "claude-code";