Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions backend/internal/service/session/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
15 changes: 15 additions & 0 deletions backend/internal/service/session/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"strings"
"testing"
"time"

Expand Down Expand Up @@ -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"}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -142,4 +142,4 @@ export const RequiredAgentField = memo(function RequiredAgentField({
</Select>
</div>
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -210,4 +210,4 @@ describe("ProjectSettingsForm", () => {
});
expect(await screen.findByText("Saved.")).toBeInTheDocument();
});
});
});
2 changes: 1 addition & 1 deletion frontend/src/renderer/components/ProjectSettingsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -300,4 +300,4 @@ function CenteredNote({ children }: { children: React.ReactNode }) {
// rather than an empty {} the daemon would persist.
function blankToUndefined<T extends object>(obj: T): T | undefined {
return Object.values(obj).some((v) => v !== undefined) ? obj : undefined;
}
}
2 changes: 1 addition & 1 deletion frontend/src/renderer/lib/agent-options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
export const DEFAULT_PROJECT_AGENT: (typeof AGENT_OPTIONS)[number] = "claude-code";